From 1aadd36644a8b0a1cf4958d888f70f84947ba8a9 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Thu, 29 Oct 2020 15:54:39 -0700 Subject: [PATCH] MM-28859 Add feature flag managment system using split.io and remove viper. (#15954) * Add feature flag managment system using split.io and remove viper. * Fixing tests. * Attempt to fix postgres tests. * Fix watch filepath for advanced logging. * Review fixes. * Some error wrapping. * Remove unessisary store interface. * Desanitize SplitKey * Simplify. * Review feedback. * Rename split mlog adatper to split logger. * fsInner * Style. * Restore oldcfg test. * Downgrading non-actionable feature flag errors to warnings. Co-authored-by: Mattermod --- .gitignore | 2 +- api4/apitestlib.go | 32 +- api4/system_test.go | 13 +- app/feature_flags.go | 102 + app/helper_test.go | 11 +- app/notification_push_test.go | 7 +- app/options.go | 2 +- app/server.go | 83 +- app/server_test.go | 40 +- app/slashcommands/helper_test.go | 5 +- cmd/mattermost/commands/config.go | 7 +- cmd/mattermost/commands/config_test.go | 1 + cmd/mattermost/commands/init.go | 6 +- cmd/mattermost/commands/jobserver.go | 6 +- cmd/mattermost/commands/plugin_test.go | 16 +- cmd/mattermost/commands/root.go | 7 +- cmd/mattermost/commands/server.go | 7 +- cmd/mattermost/commands/server_test.go | 13 +- cmd/mattermost/commands/utils.go | 17 + config/common.go | 177 -- config/common_test.go | 69 +- config/database.go | 50 +- config/database_test.go | 146 +- config/environment.go | 111 + config/environment_test.go | 130 +- config/export_test.go | 7 - config/feature_flags.go | 99 + config/feature_flags_test.go | 75 + config/file.go | 93 +- config/file_test.go | 216 +- config/memory.go | 53 +- config/memory_test.go | 250 -- config/migrate.go | 2 +- config/migrate_test.go | 20 +- config/split_logger.go | 35 + config/store.go | 265 +- config/unmarshal.go | 117 - config/unmarshal_test.go | 354 --- config/utils.go | 4 + config/utils_test.go | 1 + go.mod | 7 +- go.sum | 165 +- migrations/helper_test.go | 6 +- model/config.go | 20 +- services/mailservice/mail_test.go | 37 +- .../go-rendezvous}/LICENSE | 2 +- .../github.com/dgryski/go-rendezvous/rdv.go | 79 + .../github.com/go-redis/redis/v8/.gitignore | 3 + .../go-redis/redis/v8/.golangci.yml | 21 + .../github.com/go-redis/redis/v8/.prettierrc | 4 + .../github.com/go-redis/redis/v8/.travis.yml | 20 + .../github.com/go-redis/redis/v8/CHANGELOG.md | 95 + vendor/github.com/go-redis/redis/v8/LICENSE | 25 + vendor/github.com/go-redis/redis/v8/Makefile | 20 + vendor/github.com/go-redis/redis/v8/README.md | 151 + .../github.com/go-redis/redis/v8/cluster.go | 1703 +++++++++++ .../go-redis/redis/v8/cluster_commands.go | 25 + .../github.com/go-redis/redis/v8/command.go | 2285 ++++++++++++++ .../github.com/go-redis/redis/v8/commands.go | 2690 +++++++++++++++++ vendor/github.com/go-redis/redis/v8/doc.go | 4 + vendor/github.com/go-redis/redis/v8/error.go | 119 + vendor/github.com/go-redis/redis/v8/go.mod | 16 + vendor/github.com/go-redis/redis/v8/go.sum | 122 + .../go-redis/redis/v8/internal/arg.go | 147 + .../redis/v8/internal/hashtag/hashtag.go | 78 + .../go-redis/redis/v8/internal/instruments.go | 33 + .../go-redis/redis/v8/internal/internal.go | 25 + .../go-redis/redis/v8/internal/log.go | 24 + .../go-redis/redis/v8/internal/once.go | 60 + .../go-redis/redis/v8/internal/pool/conn.go | 132 + .../go-redis/redis/v8/internal/pool/pool.go | 524 ++++ .../redis/v8/internal/pool/pool_single.go | 58 + .../redis/v8/internal/pool/pool_sticky.go | 202 ++ .../redis/v8/internal/proto/reader.go | 314 ++ .../go-redis/redis/v8/internal/proto/scan.go | 166 + .../redis/v8/internal/proto/writer.go | 153 + .../go-redis/redis/v8/internal/safe.go | 11 + .../go-redis/redis/v8/internal/unsafe.go | 20 + .../go-redis/redis/v8/internal/util.go | 81 + .../go-redis/redis/v8/internal/util/safe.go | 11 + .../redis/v8/internal/util/strconv.go | 19 + .../go-redis/redis/v8/internal/util/unsafe.go | 22 + .../github.com/go-redis/redis/v8/iterator.go | 77 + .../github.com/go-redis/redis/v8/options.go | 269 ++ .../github.com/go-redis/redis/v8/pipeline.go | 137 + vendor/github.com/go-redis/redis/v8/pubsub.go | 633 ++++ vendor/github.com/go-redis/redis/v8/redis.go | 744 +++++ .../go-redis/redis/v8/renovate.json | 5 + vendor/github.com/go-redis/redis/v8/result.go | 180 ++ vendor/github.com/go-redis/redis/v8/ring.go | 743 +++++ vendor/github.com/go-redis/redis/v8/script.go | 65 + .../github.com/go-redis/redis/v8/sentinel.go | 730 +++++ vendor/github.com/go-redis/redis/v8/tx.go | 151 + .../github.com/go-redis/redis/v8/universal.go | 205 ++ vendor/github.com/hashicorp/hcl/.gitignore | 9 - vendor/github.com/hashicorp/hcl/.travis.yml | 13 - vendor/github.com/hashicorp/hcl/LICENSE | 354 --- vendor/github.com/hashicorp/hcl/Makefile | 18 - vendor/github.com/hashicorp/hcl/README.md | 125 - vendor/github.com/hashicorp/hcl/appveyor.yml | 19 - vendor/github.com/hashicorp/hcl/decoder.go | 729 ----- vendor/github.com/hashicorp/hcl/go.mod | 3 - vendor/github.com/hashicorp/hcl/go.sum | 2 - vendor/github.com/hashicorp/hcl/hcl.go | 11 - .../github.com/hashicorp/hcl/hcl/ast/ast.go | 219 -- .../github.com/hashicorp/hcl/hcl/ast/walk.go | 52 - .../hashicorp/hcl/hcl/parser/error.go | 17 - .../hashicorp/hcl/hcl/parser/parser.go | 532 ---- .../hashicorp/hcl/hcl/printer/nodes.go | 789 ----- .../hashicorp/hcl/hcl/printer/printer.go | 66 - .../hashicorp/hcl/hcl/scanner/scanner.go | 652 ---- .../hashicorp/hcl/hcl/strconv/quote.go | 241 -- .../hashicorp/hcl/hcl/token/position.go | 46 - .../hashicorp/hcl/hcl/token/token.go | 219 -- .../hashicorp/hcl/json/parser/flatten.go | 117 - .../hashicorp/hcl/json/parser/parser.go | 313 -- .../hashicorp/hcl/json/scanner/scanner.go | 451 --- .../hashicorp/hcl/json/token/position.go | 46 - .../hashicorp/hcl/json/token/token.go | 118 - vendor/github.com/hashicorp/hcl/lex.go | 38 - vendor/github.com/hashicorp/hcl/parse.go | 39 - .../magiconair/properties/.gitignore | 6 - .../magiconair/properties/.travis.yml | 16 - .../magiconair/properties/CHANGELOG.md | 160 - .../magiconair/properties/LICENSE.md | 24 - .../magiconair/properties/README.md | 128 - .../magiconair/properties/decode.go | 289 -- .../github.com/magiconair/properties/doc.go | 156 - .../github.com/magiconair/properties/go.mod | 3 - .../magiconair/properties/integrate.go | 34 - .../github.com/magiconair/properties/lex.go | 407 --- .../github.com/magiconair/properties/load.go | 293 -- .../magiconair/properties/parser.go | 95 - .../magiconair/properties/properties.go | 854 ------ .../magiconair/properties/rangecheck.go | 31 - vendor/github.com/mattermost/viper/.gitignore | 29 - .../github.com/mattermost/viper/.travis.yml | 31 - vendor/github.com/mattermost/viper/LICENSE | 21 - vendor/github.com/mattermost/viper/README.md | 691 ----- vendor/github.com/mattermost/viper/flags.go | 57 - vendor/github.com/mattermost/viper/go.mod | 24 - vendor/github.com/mattermost/viper/go.sum | 38 - vendor/github.com/mattermost/viper/util.go | 221 -- vendor/github.com/mattermost/viper/viper.go | 1856 ------------ .../mitchellh/mapstructure/.travis.yml | 9 - .../mitchellh/mapstructure/CHANGELOG.md | 61 - .../mitchellh/mapstructure/README.md | 46 - .../mitchellh/mapstructure/decode_hooks.go | 217 -- .../mitchellh/mapstructure/error.go | 50 - .../github.com/mitchellh/mapstructure/go.mod | 3 - .../mitchellh/mapstructure/mapstructure.go | 1417 --------- vendor/github.com/spf13/afero/.gitignore | 2 - vendor/github.com/spf13/afero/.travis.yml | 22 - vendor/github.com/spf13/afero/README.md | 429 --- vendor/github.com/spf13/afero/afero.go | 108 - vendor/github.com/spf13/afero/appveyor.yml | 15 - vendor/github.com/spf13/afero/basepath.go | 206 -- .../github.com/spf13/afero/cacheOnReadFs.go | 290 -- .../github.com/spf13/afero/copyOnWriteFs.go | 313 -- vendor/github.com/spf13/afero/go.mod | 9 - vendor/github.com/spf13/afero/go.sum | 29 - vendor/github.com/spf13/afero/httpFs.go | 110 - vendor/github.com/spf13/afero/ioutil.go | 240 -- vendor/github.com/spf13/afero/match.go | 110 - vendor/github.com/spf13/afero/mem/dirmap.go | 43 - vendor/github.com/spf13/afero/mem/file.go | 323 -- vendor/github.com/spf13/afero/memmap.go | 394 --- vendor/github.com/spf13/afero/os.go | 109 - vendor/github.com/spf13/afero/path.go | 106 - vendor/github.com/spf13/afero/readonlyfs.go | 92 - vendor/github.com/spf13/afero/regexpfs.go | 217 -- vendor/github.com/spf13/afero/symlink.go | 55 - vendor/github.com/spf13/afero/unionFile.go | 317 -- vendor/github.com/spf13/afero/util.go | 330 -- vendor/github.com/spf13/cast/.gitignore | 25 - vendor/github.com/spf13/cast/.travis.yml | 16 - vendor/github.com/spf13/cast/LICENSE | 21 - vendor/github.com/spf13/cast/Makefile | 40 - vendor/github.com/spf13/cast/README.md | 75 - vendor/github.com/spf13/cast/cast.go | 171 -- vendor/github.com/spf13/cast/caste.go | 1249 -------- vendor/github.com/spf13/cast/go.mod | 7 - vendor/github.com/spf13/cast/go.sum | 6 - .../spf13/jwalterweatherman/.gitignore | 24 - .../spf13/jwalterweatherman/LICENSE | 21 - .../spf13/jwalterweatherman/README.md | 148 - .../jwalterweatherman/default_notepad.go | 111 - .../github.com/spf13/jwalterweatherman/go.mod | 7 - .../spf13/jwalterweatherman/log_counter.go | 46 - .../spf13/jwalterweatherman/notepad.go | 225 -- .../go-client/v6/CONTRIBUTORS-GUIDE.md | 27 + .../splitio/go-client/v6/LICENSE.txt | 13 + .../v6/splitio/client/bucketingkey.go | 16 + .../go-client/v6/splitio/client/client.go | 390 +++ .../go-client/v6/splitio/client/factory.go | 463 +++ .../v6/splitio/client/factory_tracker.go | 72 + .../v6/splitio/client/input_validator.go | 294 ++ .../go-client/v6/splitio/client/manager.go | 115 + .../go-client/v6/splitio/conf/defaults.go | 22 + .../go-client/v6/splitio/conf/sdkconf.go | 259 ++ .../splitio/go-client/v6/splitio/conf/util.go | 48 + .../go-client/v6/splitio/engine/engine.go | 70 + .../v6/splitio/engine/evaluator/evaluator.go | 160 + .../impressionlabels/impression_labels.go | 22 + .../v6/splitio/engine/evaluator/interface.go | 7 + .../v6/splitio/engine/grammar/condition.go | 99 + .../v6/splitio/engine/grammar/constants.go | 22 + .../engine/grammar/matchers/allkeys.go | 16 + .../engine/grammar/matchers/allofset.go | 56 + .../engine/grammar/matchers/anyofset.go | 51 + .../engine/grammar/matchers/between.go | 70 + .../engine/grammar/matchers/boolean.go | 60 + .../engine/grammar/matchers/contains.go | 45 + .../grammar/matchers/datatypes/datatypes.go | 33 + .../engine/grammar/matchers/dependency.go | 43 + .../engine/grammar/matchers/endswith.go | 45 + .../engine/grammar/matchers/equalto.go | 65 + .../engine/grammar/matchers/equaltoset.go | 50 + .../splitio/engine/grammar/matchers/gtoet.go | 59 + .../engine/grammar/matchers/insegment.go | 39 + .../splitio/engine/grammar/matchers/ltoet.go | 61 + .../engine/grammar/matchers/matchers.go | 350 +++ .../engine/grammar/matchers/partofset.go | 52 + .../splitio/engine/grammar/matchers/regex.go | 48 + .../engine/grammar/matchers/startswith.go | 45 + .../engine/grammar/matchers/whitelist.go | 43 + .../v6/splitio/engine/grammar/split.go | 94 + .../v6/splitio/engine/hash/legacy.go | 10 + .../v6/splitio/engine/hash/murmur.go | 66 + .../impressionListener/impression_listener.go | 6 + .../impressions_listener_wrapper.go | 41 + .../splitio/go-client/v6/splitio/version.go | 4 + .../splitio/go-split-commons/v2/LICENSE | 13 + .../splitio/go-split-commons/v2/conf/conf.go | 89 + .../go-split-commons/v2/conf/defaults.go | 52 + .../splitio/go-split-commons/v2/dtos/event.go | 35 + .../splitio/go-split-commons/v2/dtos/http.go | 12 + .../go-split-commons/v2/dtos/impression.go | 48 + .../go-split-commons/v2/dtos/metadata.go | 8 + .../go-split-commons/v2/dtos/metrics.go | 132 + .../go-split-commons/v2/dtos/notification.go | 149 + .../go-split-commons/v2/dtos/segment.go | 22 + .../splitio/go-split-commons/v2/dtos/split.go | 100 + .../splitio/go-split-commons/v2/dtos/token.go | 103 + .../v2/provisional/impcounter.go | 59 + .../v2/provisional/imphasher.go | 43 + .../v2/provisional/impmanager.go | 76 + .../v2/provisional/impobserver.go | 61 + .../splitio/go-split-commons/v2/push/dtos.go | 27 + .../go-split-commons/v2/push/eventhandler.go | 89 + .../go-split-commons/v2/push/interface.go | 10 + .../go-split-commons/v2/push/keeper.go | 98 + .../go-split-commons/v2/push/parser.go | 64 + .../go-split-commons/v2/push/processor.go | 112 + .../go-split-commons/v2/push/pushmanager.go | 370 +++ .../go-split-commons/v2/push/segment.go | 76 + .../splitio/go-split-commons/v2/push/split.go | 77 + .../go-split-commons/v2/service/api/auth.go | 39 + .../go-split-commons/v2/service/api/client.go | 157 + .../v2/service/api/http_fetchers.go | 110 + .../v2/service/api/http_recorders.go | 197 ++ .../v2/service/api/sse/client.go | 127 + .../go-split-commons/v2/service/interfaces.go | 38 + .../v2/service/local/splitFetcher.go | 258 ++ .../go-split-commons/v2/service/split.go | 35 + .../go-split-commons/v2/storage/interfaces.go | 118 + .../v2/storage/metricWrapper.go | 125 + .../v2/storage/mocks/event.go | 43 + .../v2/storage/mocks/impression.go | 43 + .../v2/storage/mocks/metric.go | 73 + .../v2/storage/mocks/segment.go | 43 + .../v2/storage/mocks/split.go | 76 + .../v2/storage/mutexmap/metrics.go | 149 + .../v2/storage/mutexmap/segments.go | 88 + .../v2/storage/mutexmap/splits.go | 194 ++ .../v2/storage/mutexqueue/constants.go | 6 + .../v2/storage/mutexqueue/events.go | 136 + .../v2/storage/mutexqueue/impressions.go | 108 + .../v2/storage/redis/constants.go | 17 + .../v2/storage/redis/events.go | 177 ++ .../v2/storage/redis/impressions.go | 149 + .../v2/storage/redis/metrics.go | 322 ++ .../v2/storage/redis/miscstorage.go | 56 + .../v2/storage/redis/redis.go | 74 + .../v2/storage/redis/segments.go | 100 + .../v2/storage/redis/splits.go | 261 ++ .../v2/synchronizer/interface.go | 12 + .../go-split-commons/v2/synchronizer/local.go | 80 + .../v2/synchronizer/manager.go | 184 ++ .../v2/synchronizer/synchronizer.go | 158 + .../v2/synchronizer/worker/event/interface.go | 7 + .../v2/synchronizer/worker/event/single.go | 76 + .../worker/impression/interface.go | 7 + .../synchronizer/worker/impression/single.go | 117 + .../worker/impressionscount/interface.go | 6 + .../worker/impressionscount/single.go | 51 + .../synchronizer/worker/metric/interface.go | 6 + .../v2/synchronizer/worker/metric/single.go | 74 + .../synchronizer/worker/segment/interface.go | 8 + .../v2/synchronizer/worker/segment/segment.go | 139 + .../v2/synchronizer/worker/split/interface.go | 6 + .../v2/synchronizer/worker/split/split.go | 84 + .../go-split-commons/v2/tasks/eventsync.go | 52 + .../v2/tasks/impressionscountsync.go | 27 + .../v2/tasks/impressionsync.go | 52 + .../go-split-commons/v2/tasks/interface.go | 53 + .../go-split-commons/v2/tasks/metricsync.go | 23 + .../go-split-commons/v2/tasks/segmentsync.go | 81 + .../go-split-commons/v2/tasks/splitsync.go | 20 + .../go-split-commons/v2/tasks/worker.go | 47 + .../go-split-commons/v2/util/metrics.go | 43 + .../splitio/go-split-commons/v2/util/mode.go | 30 + .../splitio/go-split-commons/v2/util/time.go | 11 + .../github.com/splitio/go-toolkit/v3/LICENSE | 13 + .../go-toolkit/v3/asynctask/asynctasks.go | 179 ++ .../splitio/go-toolkit/v3/common/iterutil.go | 67 + .../splitio/go-toolkit/v3/common/refutil.go | 105 + .../splitio/go-toolkit/v3/common/sliceutil.go | 18 + .../splitio/go-toolkit/v3/common/strutil.go | 9 + .../splitio/go-toolkit/v3/common/timeutil.go | 25 + .../v3/datastructures/set/functions.go | 56 + .../v3/datastructures/set/implementations.go | 355 +++ .../go-toolkit/v3/datastructures/set/set.go | 24 + .../go-toolkit/v3/injection/container.go | 47 + .../go-toolkit/v3/logging/functions.go | 33 + .../go-toolkit/v3/logging/interface.go | 12 + .../splitio/go-toolkit/v3/logging/levels.go | 93 + .../splitio/go-toolkit/v3/logging/logging.go | 129 + .../splitio/go-toolkit/v3/logging/rotate.go | 106 + .../splitio/go-toolkit/v3/nethelpers/ip.go | 44 + .../v3/provisional/hashing/murmur128.go | 231 ++ .../v3/provisional/int64cache/cache.go | 91 + .../splitio/go-toolkit/v3/queuecache/cache.go | 127 + .../go-toolkit/v3/redis/helpers/helpers.go | 23 + .../go-toolkit/v3/redis/prefixedclient.go | 160 + .../splitio/go-toolkit/v3/redis/types.go | 8 + .../splitio/go-toolkit/v3/redis/wrapper.go | 293 ++ .../splitio/go-toolkit/v3/sse/sse.go | 214 ++ .../go-toolkit/v3/workerpool/workerpool.go | 166 + vendor/go.opentelemetry.io/otel/.gitignore | 22 + vendor/go.opentelemetry.io/otel/.gitmodules | 3 + vendor/go.opentelemetry.io/otel/.golangci.yml | 32 + vendor/go.opentelemetry.io/otel/CHANGELOG.md | 803 +++++ vendor/go.opentelemetry.io/otel/CODEOWNERS | 17 + .../go.opentelemetry.io/otel/CONTRIBUTING.md | 361 +++ .../otel/LICENSE} | 29 +- vendor/go.opentelemetry.io/otel/Makefile | 157 + .../go.opentelemetry.io/otel/Makefile.proto | 72 + vendor/go.opentelemetry.io/otel/README.md | 80 + vendor/go.opentelemetry.io/otel/RELEASING.md | 81 + .../otel/api/correlation/context.go | 172 ++ .../correlation_context_propagator.go | 118 + .../otel/api/correlation/doc.go | 19 + .../otel/api/correlation/map.go | 176 ++ .../otel/api/global/handler.go | 91 + .../otel/api/global/internal/meter.go | 347 +++ .../otel/api/global/internal/state.go | 140 + .../otel/api/global/internal/trace.go | 124 + .../otel/api/global/metric.go | 49 + .../otel/api/global/propagation.go | 32 + .../otel/api/global/trace.go | 44 + .../otel/api/metric/async.go | 217 ++ .../otel/api/metric/config.go | 125 + .../otel/api/metric/counter.go | 95 + .../otel/api/metric/descriptor.go | 77 + .../otel/api/metric/doc.go | 49 + .../otel/api/metric/kind.go | 79 + .../otel/api/metric/kind_string.go | 28 + .../otel/api/metric/meter.go | 320 ++ .../otel/api/metric/must.go | 222 ++ .../otel/api/metric/noop.go | 58 + .../otel/api/metric/number.go | 540 ++++ .../otel/api/metric/numberkind_string.go | 24 + .../otel/api/metric/observer.go | 124 + .../otel/api/metric/registry/registry.go | 170 ++ .../otel/api/metric/sdkapi.go | 94 + .../otel/api/metric/sync.go | 192 ++ .../otel/api/metric/updowncounter.go | 96 + .../otel/api/metric/valuerecorder.go | 97 + .../otel/api/propagation/doc.go} | 20 +- .../otel/api/propagation/propagation.go | 143 + .../go.opentelemetry.io/otel/api/trace/api.go | 273 ++ .../otel/api/trace/b3_propagator.go | 343 +++ .../otel/api/trace/context.go | 55 + .../go.opentelemetry.io/otel/api/trace/doc.go | 15 + .../otel/api/trace/noop_span.go | 79 + .../otel/api/trace/noop_trace.go} | 34 +- .../otel/api/trace/noop_trace_provider.go | 24 + .../otel/api/trace/span_context.go | 197 ++ .../api/trace/trace_context_propagator.go | 152 + .../go.opentelemetry.io/otel/api/unit/doc.go | 15 + .../otel/api/unit/unit.go} | 17 +- .../go.opentelemetry.io/otel/codes/codes.go | 89 + vendor/go.opentelemetry.io/otel/doc.go | 15 + .../go.opentelemetry.io/otel/error_handler.go | 22 + .../go.opentelemetry.io/otel/get_main_pkgs.sh | 36 + vendor/go.opentelemetry.io/otel/go.mod | 8 + vendor/go.opentelemetry.io/otel/go.sum | 15 + .../otel/internal/rawhelpers.go | 91 + vendor/go.opentelemetry.io/otel/label/doc.go | 16 + .../go.opentelemetry.io/otel/label/encoder.go | 150 + .../otel/label/iterator.go | 143 + vendor/go.opentelemetry.io/otel/label/key.go | 169 ++ vendor/go.opentelemetry.io/otel/label/kv.go | 144 + vendor/go.opentelemetry.io/otel/label/set.go | 468 +++ .../otel/label/type_string.go | 32 + .../go.opentelemetry.io/otel/label/value.go | 284 ++ .../otel/otel.go} | 24 +- .../go.opentelemetry.io/otel/pre_release.sh | 95 + vendor/go.opentelemetry.io/otel/tag.sh | 178 ++ .../otel/verify_examples.sh | 85 + vendor/golang.org/x/exp/AUTHORS | 3 + vendor/golang.org/x/exp/CONTRIBUTORS | 3 + vendor/golang.org/x/exp/LICENSE | 27 + vendor/golang.org/x/exp/PATENTS | 22 + vendor/golang.org/x/exp/rand/exp.go | 222 ++ vendor/golang.org/x/exp/rand/normal.go | 157 + vendor/golang.org/x/exp/rand/rand.go | 371 +++ vendor/golang.org/x/exp/rand/rng.go | 91 + vendor/golang.org/x/exp/rand/zipf.go | 77 + vendor/golang.org/x/mod/module/module.go | 80 +- vendor/modules.txt | 106 +- web/web_test.go | 5 +- 423 files changed, 37646 insertions(+), 20257 deletions(-) create mode 100644 app/feature_flags.go delete mode 100644 config/common.go create mode 100644 config/feature_flags.go create mode 100644 config/feature_flags_test.go create mode 100644 config/split_logger.go delete mode 100644 config/unmarshal_test.go rename vendor/github.com/{mitchellh/mapstructure => dgryski/go-rendezvous}/LICENSE (94%) create mode 100644 vendor/github.com/dgryski/go-rendezvous/rdv.go create mode 100644 vendor/github.com/go-redis/redis/v8/.gitignore create mode 100644 vendor/github.com/go-redis/redis/v8/.golangci.yml create mode 100644 vendor/github.com/go-redis/redis/v8/.prettierrc create mode 100644 vendor/github.com/go-redis/redis/v8/.travis.yml create mode 100644 vendor/github.com/go-redis/redis/v8/CHANGELOG.md create mode 100644 vendor/github.com/go-redis/redis/v8/LICENSE create mode 100644 vendor/github.com/go-redis/redis/v8/Makefile create mode 100644 vendor/github.com/go-redis/redis/v8/README.md create mode 100644 vendor/github.com/go-redis/redis/v8/cluster.go create mode 100644 vendor/github.com/go-redis/redis/v8/cluster_commands.go create mode 100644 vendor/github.com/go-redis/redis/v8/command.go create mode 100644 vendor/github.com/go-redis/redis/v8/commands.go create mode 100644 vendor/github.com/go-redis/redis/v8/doc.go create mode 100644 vendor/github.com/go-redis/redis/v8/error.go create mode 100644 vendor/github.com/go-redis/redis/v8/go.mod create mode 100644 vendor/github.com/go-redis/redis/v8/go.sum create mode 100644 vendor/github.com/go-redis/redis/v8/internal/arg.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/hashtag/hashtag.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/instruments.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/internal.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/log.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/once.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/pool/conn.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/pool/pool.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/pool/pool_single.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/pool/pool_sticky.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/proto/reader.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/proto/scan.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/proto/writer.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/safe.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/unsafe.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/util.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/util/safe.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/util/strconv.go create mode 100644 vendor/github.com/go-redis/redis/v8/internal/util/unsafe.go create mode 100644 vendor/github.com/go-redis/redis/v8/iterator.go create mode 100644 vendor/github.com/go-redis/redis/v8/options.go create mode 100644 vendor/github.com/go-redis/redis/v8/pipeline.go create mode 100644 vendor/github.com/go-redis/redis/v8/pubsub.go create mode 100644 vendor/github.com/go-redis/redis/v8/redis.go create mode 100644 vendor/github.com/go-redis/redis/v8/renovate.json create mode 100644 vendor/github.com/go-redis/redis/v8/result.go create mode 100644 vendor/github.com/go-redis/redis/v8/ring.go create mode 100644 vendor/github.com/go-redis/redis/v8/script.go create mode 100644 vendor/github.com/go-redis/redis/v8/sentinel.go create mode 100644 vendor/github.com/go-redis/redis/v8/tx.go create mode 100644 vendor/github.com/go-redis/redis/v8/universal.go delete mode 100644 vendor/github.com/hashicorp/hcl/.gitignore delete mode 100644 vendor/github.com/hashicorp/hcl/.travis.yml delete mode 100644 vendor/github.com/hashicorp/hcl/LICENSE delete mode 100644 vendor/github.com/hashicorp/hcl/Makefile delete mode 100644 vendor/github.com/hashicorp/hcl/README.md delete mode 100644 vendor/github.com/hashicorp/hcl/appveyor.yml delete mode 100644 vendor/github.com/hashicorp/hcl/decoder.go delete mode 100644 vendor/github.com/hashicorp/hcl/go.mod delete mode 100644 vendor/github.com/hashicorp/hcl/go.sum delete mode 100644 vendor/github.com/hashicorp/hcl/hcl.go delete mode 100644 vendor/github.com/hashicorp/hcl/hcl/ast/ast.go delete mode 100644 vendor/github.com/hashicorp/hcl/hcl/ast/walk.go delete mode 100644 vendor/github.com/hashicorp/hcl/hcl/parser/error.go delete mode 100644 vendor/github.com/hashicorp/hcl/hcl/parser/parser.go delete mode 100644 vendor/github.com/hashicorp/hcl/hcl/printer/nodes.go delete mode 100644 vendor/github.com/hashicorp/hcl/hcl/printer/printer.go delete mode 100644 vendor/github.com/hashicorp/hcl/hcl/scanner/scanner.go delete mode 100644 vendor/github.com/hashicorp/hcl/hcl/strconv/quote.go delete mode 100644 vendor/github.com/hashicorp/hcl/hcl/token/position.go delete mode 100644 vendor/github.com/hashicorp/hcl/hcl/token/token.go delete mode 100644 vendor/github.com/hashicorp/hcl/json/parser/flatten.go delete mode 100644 vendor/github.com/hashicorp/hcl/json/parser/parser.go delete mode 100644 vendor/github.com/hashicorp/hcl/json/scanner/scanner.go delete mode 100644 vendor/github.com/hashicorp/hcl/json/token/position.go delete mode 100644 vendor/github.com/hashicorp/hcl/json/token/token.go delete mode 100644 vendor/github.com/hashicorp/hcl/lex.go delete mode 100644 vendor/github.com/hashicorp/hcl/parse.go delete mode 100644 vendor/github.com/magiconair/properties/.gitignore delete mode 100644 vendor/github.com/magiconair/properties/.travis.yml delete mode 100644 vendor/github.com/magiconair/properties/CHANGELOG.md delete mode 100644 vendor/github.com/magiconair/properties/LICENSE.md delete mode 100644 vendor/github.com/magiconair/properties/README.md delete mode 100644 vendor/github.com/magiconair/properties/decode.go delete mode 100644 vendor/github.com/magiconair/properties/doc.go delete mode 100644 vendor/github.com/magiconair/properties/go.mod delete mode 100644 vendor/github.com/magiconair/properties/integrate.go delete mode 100644 vendor/github.com/magiconair/properties/lex.go delete mode 100644 vendor/github.com/magiconair/properties/load.go delete mode 100644 vendor/github.com/magiconair/properties/parser.go delete mode 100644 vendor/github.com/magiconair/properties/properties.go delete mode 100644 vendor/github.com/magiconair/properties/rangecheck.go delete mode 100644 vendor/github.com/mattermost/viper/.gitignore delete mode 100644 vendor/github.com/mattermost/viper/.travis.yml delete mode 100644 vendor/github.com/mattermost/viper/LICENSE delete mode 100644 vendor/github.com/mattermost/viper/README.md delete mode 100644 vendor/github.com/mattermost/viper/flags.go delete mode 100644 vendor/github.com/mattermost/viper/go.mod delete mode 100644 vendor/github.com/mattermost/viper/go.sum delete mode 100644 vendor/github.com/mattermost/viper/util.go delete mode 100644 vendor/github.com/mattermost/viper/viper.go delete mode 100644 vendor/github.com/mitchellh/mapstructure/.travis.yml delete mode 100644 vendor/github.com/mitchellh/mapstructure/CHANGELOG.md delete mode 100644 vendor/github.com/mitchellh/mapstructure/README.md delete mode 100644 vendor/github.com/mitchellh/mapstructure/decode_hooks.go delete mode 100644 vendor/github.com/mitchellh/mapstructure/error.go delete mode 100644 vendor/github.com/mitchellh/mapstructure/go.mod delete mode 100644 vendor/github.com/mitchellh/mapstructure/mapstructure.go delete mode 100644 vendor/github.com/spf13/afero/.gitignore delete mode 100644 vendor/github.com/spf13/afero/.travis.yml delete mode 100644 vendor/github.com/spf13/afero/README.md delete mode 100644 vendor/github.com/spf13/afero/afero.go delete mode 100644 vendor/github.com/spf13/afero/appveyor.yml delete mode 100644 vendor/github.com/spf13/afero/basepath.go delete mode 100644 vendor/github.com/spf13/afero/cacheOnReadFs.go delete mode 100644 vendor/github.com/spf13/afero/copyOnWriteFs.go delete mode 100644 vendor/github.com/spf13/afero/go.mod delete mode 100644 vendor/github.com/spf13/afero/go.sum delete mode 100644 vendor/github.com/spf13/afero/httpFs.go delete mode 100644 vendor/github.com/spf13/afero/ioutil.go delete mode 100644 vendor/github.com/spf13/afero/match.go delete mode 100644 vendor/github.com/spf13/afero/mem/dirmap.go delete mode 100644 vendor/github.com/spf13/afero/mem/file.go delete mode 100644 vendor/github.com/spf13/afero/memmap.go delete mode 100644 vendor/github.com/spf13/afero/os.go delete mode 100644 vendor/github.com/spf13/afero/path.go delete mode 100644 vendor/github.com/spf13/afero/readonlyfs.go delete mode 100644 vendor/github.com/spf13/afero/regexpfs.go delete mode 100644 vendor/github.com/spf13/afero/symlink.go delete mode 100644 vendor/github.com/spf13/afero/unionFile.go delete mode 100644 vendor/github.com/spf13/afero/util.go delete mode 100644 vendor/github.com/spf13/cast/.gitignore delete mode 100644 vendor/github.com/spf13/cast/.travis.yml delete mode 100644 vendor/github.com/spf13/cast/LICENSE delete mode 100644 vendor/github.com/spf13/cast/Makefile delete mode 100644 vendor/github.com/spf13/cast/README.md delete mode 100644 vendor/github.com/spf13/cast/cast.go delete mode 100644 vendor/github.com/spf13/cast/caste.go delete mode 100644 vendor/github.com/spf13/cast/go.mod delete mode 100644 vendor/github.com/spf13/cast/go.sum delete mode 100644 vendor/github.com/spf13/jwalterweatherman/.gitignore delete mode 100644 vendor/github.com/spf13/jwalterweatherman/LICENSE delete mode 100644 vendor/github.com/spf13/jwalterweatherman/README.md delete mode 100644 vendor/github.com/spf13/jwalterweatherman/default_notepad.go delete mode 100644 vendor/github.com/spf13/jwalterweatherman/go.mod delete mode 100644 vendor/github.com/spf13/jwalterweatherman/log_counter.go delete mode 100644 vendor/github.com/spf13/jwalterweatherman/notepad.go create mode 100644 vendor/github.com/splitio/go-client/v6/CONTRIBUTORS-GUIDE.md create mode 100644 vendor/github.com/splitio/go-client/v6/LICENSE.txt create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/client/bucketingkey.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/client/client.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/client/factory.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/client/factory_tracker.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/client/input_validator.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/client/manager.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/conf/defaults.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/conf/sdkconf.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/conf/util.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/engine.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/evaluator/evaluator.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/evaluator/impressionlabels/impression_labels.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/evaluator/interface.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/condition.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/constants.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/allkeys.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/allofset.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/anyofset.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/between.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/boolean.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/contains.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/datatypes/datatypes.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/dependency.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/endswith.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/equalto.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/equaltoset.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/gtoet.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/insegment.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/ltoet.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/matchers.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/partofset.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/regex.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/startswith.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/matchers/whitelist.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/grammar/split.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/hash/legacy.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/engine/hash/murmur.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/impressionListener/impression_listener.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/impressionListener/impressions_listener_wrapper.go create mode 100644 vendor/github.com/splitio/go-client/v6/splitio/version.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/LICENSE create mode 100644 vendor/github.com/splitio/go-split-commons/v2/conf/conf.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/conf/defaults.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/dtos/event.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/dtos/http.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/dtos/impression.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/dtos/metadata.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/dtos/metrics.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/dtos/notification.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/dtos/segment.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/dtos/split.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/dtos/token.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/provisional/impcounter.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/provisional/imphasher.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/provisional/impmanager.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/provisional/impobserver.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/push/dtos.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/push/eventhandler.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/push/interface.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/push/keeper.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/push/parser.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/push/processor.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/push/pushmanager.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/push/segment.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/push/split.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/service/api/auth.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/service/api/client.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/service/api/http_fetchers.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/service/api/http_recorders.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/service/api/sse/client.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/service/interfaces.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/service/local/splitFetcher.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/service/split.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/interfaces.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/metricWrapper.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/mocks/event.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/mocks/impression.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/mocks/metric.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/mocks/segment.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/mocks/split.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/mutexmap/metrics.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/mutexmap/segments.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/mutexmap/splits.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/mutexqueue/constants.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/mutexqueue/events.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/mutexqueue/impressions.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/redis/constants.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/redis/events.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/redis/impressions.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/redis/metrics.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/redis/miscstorage.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/redis/redis.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/redis/segments.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/storage/redis/splits.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/interface.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/local.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/manager.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/synchronizer.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/worker/event/interface.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/worker/event/single.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/worker/impression/interface.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/worker/impression/single.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/worker/impressionscount/interface.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/worker/impressionscount/single.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/worker/metric/interface.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/worker/metric/single.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/worker/segment/interface.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/worker/segment/segment.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/worker/split/interface.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/synchronizer/worker/split/split.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/tasks/eventsync.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/tasks/impressionscountsync.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/tasks/impressionsync.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/tasks/interface.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/tasks/metricsync.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/tasks/segmentsync.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/tasks/splitsync.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/tasks/worker.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/util/metrics.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/util/mode.go create mode 100644 vendor/github.com/splitio/go-split-commons/v2/util/time.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/LICENSE create mode 100644 vendor/github.com/splitio/go-toolkit/v3/asynctask/asynctasks.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/common/iterutil.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/common/refutil.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/common/sliceutil.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/common/strutil.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/common/timeutil.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/datastructures/set/functions.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/datastructures/set/implementations.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/datastructures/set/set.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/injection/container.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/logging/functions.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/logging/interface.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/logging/levels.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/logging/logging.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/logging/rotate.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/nethelpers/ip.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/provisional/hashing/murmur128.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/provisional/int64cache/cache.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/queuecache/cache.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/redis/helpers/helpers.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/redis/prefixedclient.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/redis/types.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/redis/wrapper.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/sse/sse.go create mode 100644 vendor/github.com/splitio/go-toolkit/v3/workerpool/workerpool.go create mode 100644 vendor/go.opentelemetry.io/otel/.gitignore create mode 100644 vendor/go.opentelemetry.io/otel/.gitmodules create mode 100644 vendor/go.opentelemetry.io/otel/.golangci.yml create mode 100644 vendor/go.opentelemetry.io/otel/CHANGELOG.md create mode 100644 vendor/go.opentelemetry.io/otel/CODEOWNERS create mode 100644 vendor/go.opentelemetry.io/otel/CONTRIBUTING.md rename vendor/{github.com/spf13/afero/LICENSE.txt => go.opentelemetry.io/otel/LICENSE} (88%) create mode 100644 vendor/go.opentelemetry.io/otel/Makefile create mode 100644 vendor/go.opentelemetry.io/otel/Makefile.proto create mode 100644 vendor/go.opentelemetry.io/otel/README.md create mode 100644 vendor/go.opentelemetry.io/otel/RELEASING.md create mode 100644 vendor/go.opentelemetry.io/otel/api/correlation/context.go create mode 100644 vendor/go.opentelemetry.io/otel/api/correlation/correlation_context_propagator.go create mode 100644 vendor/go.opentelemetry.io/otel/api/correlation/doc.go create mode 100644 vendor/go.opentelemetry.io/otel/api/correlation/map.go create mode 100644 vendor/go.opentelemetry.io/otel/api/global/handler.go create mode 100644 vendor/go.opentelemetry.io/otel/api/global/internal/meter.go create mode 100644 vendor/go.opentelemetry.io/otel/api/global/internal/state.go create mode 100644 vendor/go.opentelemetry.io/otel/api/global/internal/trace.go create mode 100644 vendor/go.opentelemetry.io/otel/api/global/metric.go create mode 100644 vendor/go.opentelemetry.io/otel/api/global/propagation.go create mode 100644 vendor/go.opentelemetry.io/otel/api/global/trace.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/async.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/config.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/counter.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/descriptor.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/doc.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/kind.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/kind_string.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/meter.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/must.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/noop.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/number.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/numberkind_string.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/observer.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/registry/registry.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/sdkapi.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/sync.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/updowncounter.go create mode 100644 vendor/go.opentelemetry.io/otel/api/metric/valuerecorder.go rename vendor/{github.com/spf13/afero/const_win_unix.go => go.opentelemetry.io/otel/api/propagation/doc.go} (64%) create mode 100644 vendor/go.opentelemetry.io/otel/api/propagation/propagation.go create mode 100644 vendor/go.opentelemetry.io/otel/api/trace/api.go create mode 100644 vendor/go.opentelemetry.io/otel/api/trace/b3_propagator.go create mode 100644 vendor/go.opentelemetry.io/otel/api/trace/context.go create mode 100644 vendor/go.opentelemetry.io/otel/api/trace/doc.go create mode 100644 vendor/go.opentelemetry.io/otel/api/trace/noop_span.go rename vendor/{github.com/spf13/afero/mem/dir.go => go.opentelemetry.io/otel/api/trace/noop_trace.go} (52%) create mode 100644 vendor/go.opentelemetry.io/otel/api/trace/noop_trace_provider.go create mode 100644 vendor/go.opentelemetry.io/otel/api/trace/span_context.go create mode 100644 vendor/go.opentelemetry.io/otel/api/trace/trace_context_propagator.go create mode 100644 vendor/go.opentelemetry.io/otel/api/unit/doc.go rename vendor/{github.com/spf13/afero/const_bsds.go => go.opentelemetry.io/otel/api/unit/unit.go} (70%) create mode 100644 vendor/go.opentelemetry.io/otel/codes/codes.go create mode 100644 vendor/go.opentelemetry.io/otel/doc.go create mode 100644 vendor/go.opentelemetry.io/otel/error_handler.go create mode 100644 vendor/go.opentelemetry.io/otel/get_main_pkgs.sh create mode 100644 vendor/go.opentelemetry.io/otel/go.mod create mode 100644 vendor/go.opentelemetry.io/otel/go.sum create mode 100644 vendor/go.opentelemetry.io/otel/internal/rawhelpers.go create mode 100644 vendor/go.opentelemetry.io/otel/label/doc.go create mode 100644 vendor/go.opentelemetry.io/otel/label/encoder.go create mode 100644 vendor/go.opentelemetry.io/otel/label/iterator.go create mode 100644 vendor/go.opentelemetry.io/otel/label/key.go create mode 100644 vendor/go.opentelemetry.io/otel/label/kv.go create mode 100644 vendor/go.opentelemetry.io/otel/label/set.go create mode 100644 vendor/go.opentelemetry.io/otel/label/type_string.go create mode 100644 vendor/go.opentelemetry.io/otel/label/value.go rename vendor/{github.com/spf13/afero/lstater.go => go.opentelemetry.io/otel/otel.go} (50%) create mode 100644 vendor/go.opentelemetry.io/otel/pre_release.sh create mode 100644 vendor/go.opentelemetry.io/otel/tag.sh create mode 100644 vendor/go.opentelemetry.io/otel/verify_examples.sh create mode 100644 vendor/golang.org/x/exp/AUTHORS create mode 100644 vendor/golang.org/x/exp/CONTRIBUTORS create mode 100644 vendor/golang.org/x/exp/LICENSE create mode 100644 vendor/golang.org/x/exp/PATENTS create mode 100644 vendor/golang.org/x/exp/rand/exp.go create mode 100644 vendor/golang.org/x/exp/rand/normal.go create mode 100644 vendor/golang.org/x/exp/rand/rand.go create mode 100644 vendor/golang.org/x/exp/rand/rng.go create mode 100644 vendor/golang.org/x/exp/rand/zipf.go diff --git a/.gitignore b/.gitignore index 04d6a57ff3..871b5a3113 100644 --- a/.gitignore +++ b/.gitignore @@ -109,7 +109,7 @@ webapp/coverage .idea debug -client +/client __debug_bin report.xml go.*.orig diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 263b8d209c..06820a81b5 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -38,7 +38,7 @@ import ( type TestHelper struct { App *app.App Server *app.Server - ConfigStore config.Store + ConfigStore *config.Store Client *model.Client4 BasicUser *model.User @@ -79,20 +79,26 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent panic("failed to initialize memory store: " + err.Error()) } - config := memoryStore.Get() - *config.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins") - *config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp") - config.ServiceSettings.EnableLocalMode = model.NewBool(true) - *config.ServiceSettings.LocalModeSocketLocation = filepath.Join(tempWorkspace, "mattermost_local.sock") - *config.AnnouncementSettings.AdminNoticesEnabled = false - *config.AnnouncementSettings.UserNoticesEnabled = false + memoryConfig := &model.Config{} + memoryConfig.SetDefaults() + *memoryConfig.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins") + *memoryConfig.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp") + memoryConfig.ServiceSettings.EnableLocalMode = model.NewBool(true) + *memoryConfig.ServiceSettings.LocalModeSocketLocation = filepath.Join(tempWorkspace, "mattermost_local.sock") + *memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false + *memoryConfig.AnnouncementSettings.UserNoticesEnabled = false if updateConfig != nil { - updateConfig(config) + updateConfig(memoryConfig) + } + memoryStore.Set(memoryConfig) + + configStore, err := config.NewStoreFromBacking(memoryStore) + if err != nil { + panic(err) } - memoryStore.Set(config) var options []app.Option - options = append(options, app.ConfigStore(memoryStore)) + options = append(options, app.ConfigStore(configStore)) options = append(options, app.StoreOverride(dbStore)) s, err := app.NewServer(options...) @@ -107,7 +113,7 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent th := &TestHelper{ App: app.New(app.ServerConnector(s)), Server: s, - ConfigStore: memoryStore, + ConfigStore: configStore, IncludeCacheLayer: includeCache, } @@ -168,7 +174,7 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent th.Client.SetBoolString(true, trueString) th.Client.SetBoolString(false, falseString) - th.LocalClient = th.CreateLocalClient(*config.ServiceSettings.LocalModeSocketLocation) + th.LocalClient = th.CreateLocalClient(*memoryConfig.ServiceSettings.LocalModeSocketLocation) if th.tempWorkspace == "" { th.tempWorkspace = tempWorkspace diff --git a/api4/system_test.go b/api4/system_test.go index f950d86276..46f4d158b2 100644 --- a/api4/system_test.go +++ b/api4/system_test.go @@ -14,7 +14,6 @@ import ( "testing" "time" - "github.com/mattermost/mattermost-server/v5/config" "github.com/mattermost/mattermost-server/v5/mlog" "github.com/mattermost/mattermost-server/v5/model" "github.com/stretchr/testify/assert" @@ -68,6 +67,7 @@ func TestGetPing(t *testing.T) { }, "with server status") th.TestForAllClients(t, func(t *testing.T, client *model.Client4) { + th.App.ReloadConfig() resp, appErr := client.DoApiGet(client.GetSystemRoute()+"/ping", "") require.Nil(t, appErr) require.Equal(t, http.StatusOK, resp.StatusCode) @@ -77,15 +77,9 @@ func TestGetPing(t *testing.T) { require.NotContains(t, respString, "TestFeatureFlag") // Run the enviroment variable override code to test - os.Setenv("MM_FEATUREFLAGS_TESTFEATURE", "testvalue") + os.Setenv("MM_FEATUREFLAGS_TESTFEATURE", "testvalueunique") defer os.Unsetenv("MM_FEATUREFLAGS_TESTFEATURE") - memoryStore, err := config.NewMemoryStore() - require.Nil(t, err) - retrievedConfig := memoryStore.Get() - - // replace config with generated config - oldConfig := th.App.Config().Clone() - th.App.UpdateConfig(func(cfg *model.Config) { *cfg = *retrievedConfig }) + th.App.ReloadConfig() resp, appErr = client.DoApiGet(client.GetSystemRoute()+"/ping", "") require.Nil(t, appErr) @@ -94,7 +88,6 @@ func TestGetPing(t *testing.T) { require.Nil(t, err) respString = string(respBytes) require.Contains(t, respString, "testvalue") - th.App.UpdateConfig(func(cfg *model.Config) { *cfg = *oldConfig }) }, "ping feature flag test") } diff --git a/app/feature_flags.go b/app/feature_flags.go new file mode 100644 index 0000000000..dbcb0df0b0 --- /dev/null +++ b/app/feature_flags.go @@ -0,0 +1,102 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "time" + + "github.com/mattermost/mattermost-server/v5/config" + "github.com/mattermost/mattermost-server/v5/mlog" +) + +// setupFeatureFlags called on startup and when the cluster leader changes. +// Starts or stops the synchronization of feature flags from upstream management. +func (s *Server) setupFeatureFlags() { + s.featureFlagSynchronizerMutex.Lock() + defer s.featureFlagSynchronizerMutex.Unlock() + license := s.License() + inCloud := license != nil && *license.Features.Cloud + splitKey := *s.Config().ServiceSettings.SplitKey + syncFeatureFlags := inCloud && splitKey != "" && s.IsLeader() + + s.configStore.PersistFeatures(inCloud) + + if syncFeatureFlags { + if err := s.startFeatureFlagUpdateJob(); err != nil { + s.Log.Warn("Unable to setup synchronization with feature flag management. Will fallback to cloud cache.", mlog.Err(err)) + } + } else { + s.stopFeatureFlagUpdateJob() + } + + if err := s.configStore.Load(); err != nil { + s.Log.Warn("Unable to load config store after feature flag setup.", mlog.Err(err)) + } +} + +func (s *Server) updateFeatureFlagValuesFromManagment() { + newCfg := s.configStore.GetNoEnv().Clone() + oldFlags := *newCfg.FeatureFlags + newFlags := s.featureFlagSynchronizer.UpdateFeatureFlagValues(oldFlags) + if oldFlags != newFlags { + *newCfg.FeatureFlags = newFlags + s.SaveConfig(newCfg, true) + } +} + +func (s *Server) startFeatureFlagUpdateJob() error { + // Can be run multiple times + if s.featureFlagSynchronizer != nil { + return nil + } + + var log *mlog.Logger + if *s.Config().ServiceSettings.DebugSplit { + log = s.Log + } + + synchronizer, err := config.NewFeatureFlagSynchronizer(config.FeatureFlagSyncParams{ + ServerID: s.TelemetryId(), + SplitKey: *s.Config().ServiceSettings.SplitKey, + Log: log, + }) + if err != nil { + return err + } + + s.featureFlagStop = make(chan struct{}) + s.featureFlagStopped = make(chan struct{}) + s.featureFlagSynchronizer = synchronizer + syncInterval := *s.Config().ServiceSettings.FeatureFlagSyncIntervalSeconds + + go func() { + ticker := time.NewTicker(time.Duration(syncInterval) * time.Second) + defer ticker.Stop() + defer close(s.featureFlagStopped) + if err := synchronizer.EnsureReady(); err != nil { + s.Log.Warn("Problem connecting to feature flag management. Will fallback to cloud cache.", mlog.Err(err)) + return + } + s.updateFeatureFlagValuesFromManagment() + for { + select { + case <-s.featureFlagStop: + return + case <-ticker.C: + s.updateFeatureFlagValuesFromManagment() + } + } + }() + + return nil +} + +func (s *Server) stopFeatureFlagUpdateJob() { + if s.featureFlagSynchronizer != nil { + close(s.featureFlagStop) + <-s.featureFlagStopped + s.featureFlagSynchronizer.Close() + s.featureFlagSynchronizer = nil + } +} diff --git a/app/helper_test.go b/app/helper_test.go index ea47075a3f..2dd48e1eb6 100644 --- a/app/helper_test.go +++ b/app/helper_test.go @@ -48,12 +48,9 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo panic(err) } - memoryStore, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{IgnoreEnvironmentOverrides: true}) - if err != nil { - panic("failed to initialize memory store: " + err.Error()) - } + configStore := config.NewTestMemoryStore() - config := memoryStore.Get() + config := configStore.Get() if configSet != nil { configSet(config) } @@ -62,12 +59,12 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo *config.LogSettings.EnableSentry = false // disable error reporting during tests *config.AnnouncementSettings.AdminNoticesEnabled = false *config.AnnouncementSettings.UserNoticesEnabled = false - memoryStore.Set(config) + configStore.Set(config) buffer := &bytes.Buffer{} var options []Option - options = append(options, ConfigStore(memoryStore)) + options = append(options, ConfigStore(configStore)) options = append(options, StoreOverride(dbStore)) options = append(options, SetLogger(mlog.NewTestingLogger(tb, buffer))) diff --git a/app/notification_push_test.go b/app/notification_push_test.go index ac8f16191c..948946fa37 100644 --- a/app/notification_push_test.go +++ b/app/notification_push_test.go @@ -1365,12 +1365,7 @@ func TestAllPushNotifications(t *testing.T) { } func TestPushNotificationRace(t *testing.T) { - memoryStore, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{ - IgnoreEnvironmentOverrides: true, - }) - require.NoError(t, err, "failed to initialize memory store") - defer memoryStore.Close() - + memoryStore := config.NewTestMemoryStore() mockStore := testlib.GetMockStoreForSetupFunctions() mockPreferenceStore := mocks.PreferenceStore{} mockPreferenceStore.On("Get", diff --git a/app/options.go b/app/options.go index 33869823e1..2abf3cc52f 100644 --- a/app/options.go +++ b/app/options.go @@ -52,7 +52,7 @@ func Config(dsn string, watch bool) Option { } // ConfigStore applies the given config store, typically to replace the traditional sources with a memory store for testing. -func ConfigStore(configStore config.Store) Option { +func ConfigStore(configStore *config.Store) Option { return func(s *Server) error { s.configStore = configStore diff --git a/app/server.go b/app/server.go index 3ff23ec8ce..e42936c5af 100644 --- a/app/server.go +++ b/app/server.go @@ -15,6 +15,7 @@ import ( "os" "os/exec" "path" + "path/filepath" "strings" "sync" "sync/atomic" @@ -127,7 +128,7 @@ type Server struct { searchConfigListenerId string searchLicenseListenerId string loggerLicenseListenerId string - configStore config.Store + configStore *config.Store asymmetricSigningKey *ecdsa.PrivateKey postActionCookieSecret []byte @@ -178,6 +179,11 @@ type Server struct { // and data corruption. uploadLockMapMut sync.Mutex uploadLockMap map[string]bool + + featureFlagSynchronizer *config.FeatureFlagSynchronizer + featureFlagStop chan struct{} + featureFlagStopped chan struct{} + featureFlagSynchronizerMutex sync.Mutex } func NewServer(options ...Option) (*Server, error) { @@ -200,7 +206,11 @@ func NewServer(options ...Option) (*Server, error) { } if s.configStore == nil { - configStore, err := config.NewFileStore("config.json", true) + innerStore, err := config.NewFileStore("config.json", true) + if err != nil { + return nil, errors.Wrap(err, "failed to load config") + } + configStore, err := config.NewStoreFromBacking(innerStore) if err != nil { return nil, errors.Wrap(err, "failed to load config") } @@ -292,27 +302,6 @@ func NewServer(options ...Option) (*Server, error) { return nil, errors.Wrapf(err, "unable to load Mattermost translation files") } - s.configListenerId = s.AddConfigListener(func(_, _ *model.Config) { - s.configOrLicenseListener() - - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CONFIG_CHANGED, "", "", "", nil) - - message.Add("config", s.ClientConfigWithComputed()) - s.Go(func() { - s.Publish(message) - }) - }) - s.licenseListenerId = s.AddLicenseListener(func(oldLicense, newLicense *model.License) { - s.configOrLicenseListener() - - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_LICENSE_CHANGED, "", "", "", nil) - message.Add("license", s.GetSanitizedClientLicense()) - s.Go(func() { - s.Publish(message) - }) - - }) - s.initEnterprise() if s.newStore == nil { @@ -353,6 +342,27 @@ func NewServer(options ...Option) (*Server, error) { s.Store = s.newStore() + s.configListenerId = s.AddConfigListener(func(_, _ *model.Config) { + s.configOrLicenseListener() + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CONFIG_CHANGED, "", "", "", nil) + + message.Add("config", s.ClientConfigWithComputed()) + s.Go(func() { + s.Publish(message) + }) + }) + s.licenseListenerId = s.AddLicenseListener(func(oldLicense, newLicense *model.License) { + s.configOrLicenseListener() + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_LICENSE_CHANGED, "", "", "", nil) + message.Add("license", s.GetSanitizedClientLicense()) + s.Go(func() { + s.Publish(message) + }) + + }) + s.telemetryService = telemetry.New(s, s.Store, s.SearchEngine, s.Log) emailService, err := NewEmailService(s) @@ -365,8 +375,18 @@ func NewServer(options ...Option) (*Server, error) { s.LoadLicense() } + s.setupFeatureFlags() + s.initJobs() + s.clusterLeaderListenerId = s.AddClusterLeaderChangedListener(func() { + mlog.Info("Cluster leader changed. Determining if job schedulers should be running:", mlog.Bool("isLeader", s.IsLeader())) + if s.Jobs != nil && s.Jobs.Schedulers != nil { + s.Jobs.Schedulers.HandleClusterLeaderChange(s.IsLeader()) + } + s.setupFeatureFlags() + }) + if s.joinCluster && s.Cluster != nil { s.Cluster.StartInterNodeCommunication() } @@ -389,13 +409,6 @@ func NewServer(options ...Option) (*Server, error) { s.regenerateClientConfig() - s.clusterLeaderListenerId = s.AddClusterLeaderChangedListener(func() { - mlog.Info("Cluster leader changed. Determining if job schedulers should be running:", mlog.Bool("isLeader", s.IsLeader())) - if s.Jobs != nil && s.Jobs.Schedulers != nil { - s.Jobs.Schedulers.HandleClusterLeaderChange(s.IsLeader()) - } - }) - subpath, err := utils.GetSubpathFromConfig(s.Config()) if err != nil { return nil, errors.Wrap(err, "failed to parse SiteURL subpath") @@ -480,7 +493,6 @@ func NewServer(options ...Option) (*Server, error) { s.checkPushNotificationServerUrl() license := s.License() - if license == nil { s.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.MaxNotificationsPerChannel = &MaxNotificationsPerChannelDefault @@ -618,10 +630,9 @@ func (s *Server) initLogging() error { isJson := config.IsJsonMap(dsn) // If this is a file based config we need the full path so it can be watched. - if !isJson { - if fs, ok := s.configStore.(*config.FileStore); ok { - dsn = fs.GetFilePath(dsn) - } + if !isJson && strings.HasPrefix(s.configStore.String(), "file://") && !filepath.IsAbs(dsn) { + configPath := strings.TrimPrefix(s.configStore.String(), "file://") + dsn = filepath.Join(filepath.Dir(configPath), dsn) } cfg, err := config.NewLogConfigSrc(dsn, isJson, s.configStore) @@ -752,6 +763,8 @@ func (s *Server) Shutdown() error { s.Audit.Shutdown() + s.stopFeatureFlagUpdateJob() + s.configStore.Close() if s.Cluster != nil { diff --git a/app/server_test.go b/app/server_test.go index c97f9be781..fcc80ea7aa 100644 --- a/app/server_test.go +++ b/app/server_test.go @@ -65,7 +65,8 @@ func TestReadReplicaDisabledBasedOnLicense(t *testing.T) { t.Run("Read Replicas with no License", func(t *testing.T) { s, err := NewServer(func(server *Server) error { - configStore, _ := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: cfg.Clone()}) + configStore := config.NewTestMemoryStore() + configStore.Set(&cfg) server.configStore = configStore return nil }) @@ -77,8 +78,8 @@ func TestReadReplicaDisabledBasedOnLicense(t *testing.T) { t.Run("Read Replicas With License", func(t *testing.T) { s, err := NewServer(func(server *Server) error { - configStore, _ := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: cfg.Clone()}) - server.configStore = configStore + configStore := config.NewTestMemoryStore() + configStore.Set(&cfg) server.licenseValue.Store(model.NewTestLicense()) return nil }) @@ -90,7 +91,8 @@ func TestReadReplicaDisabledBasedOnLicense(t *testing.T) { t.Run("Search Replicas with no License", func(t *testing.T) { s, err := NewServer(func(server *Server) error { - configStore, _ := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: cfg.Clone()}) + configStore := config.NewTestMemoryStore() + configStore.Set(&cfg) server.configStore = configStore return nil }) @@ -102,7 +104,8 @@ func TestReadReplicaDisabledBasedOnLicense(t *testing.T) { t.Run("Search Replicas With License", func(t *testing.T) { s, err := NewServer(func(server *Server) error { - configStore, _ := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: cfg.Clone()}) + configStore := config.NewTestMemoryStore() + configStore.Set(&cfg) server.configStore = configStore server.licenseValue.Store(model.NewTestLicense()) return nil @@ -114,27 +117,6 @@ func TestReadReplicaDisabledBasedOnLicense(t *testing.T) { }) } -func TestStartServerRateLimiterCriticalError(t *testing.T) { - // Attempt to use Rate Limiter with an invalid config - ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{ - SkipValidation: true, - }) - require.NoError(t, err) - - config := ms.Get() - *config.RateLimitSettings.Enable = true - *config.RateLimitSettings.MaxBurst = -100 - _, err = ms.Set(config) - require.NoError(t, err) - - s, err := NewServer(ConfigStore(ms)) - require.NoError(t, err) - - serverErr := s.Start() - s.Shutdown() - require.Error(t, serverErr) -} - func TestStartServerPortUnavailable(t *testing.T) { s, err := NewServer() require.NoError(t, err) @@ -398,7 +380,8 @@ func TestSentry(t *testing.T) { testDir, _ := fileutils.FindDir("tests") s, err := NewServer(func(server *Server) error { configStore, _ := config.NewFileStore("config.json", true) - server.configStore = configStore + store, _ := config.NewStoreFromBacking(configStore) + server.configStore = store server.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" *cfg.LogSettings.EnableSentry = false @@ -441,7 +424,8 @@ func TestSentry(t *testing.T) { SENTRY_DSN = fmt.Sprintf("http://test:test@localhost:%s/123", port) s2, err := NewServer(func(server *Server) error { configStore, _ := config.NewFileStore("config.json", true) - server.configStore = configStore + store, _ := config.NewStoreFromBacking(configStore) + server.configStore = store server.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" *cfg.ServiceSettings.ConnectionSecurity = "TLS" diff --git a/app/slashcommands/helper_test.go b/app/slashcommands/helper_test.go index 015604034a..9e86283b80 100644 --- a/app/slashcommands/helper_test.go +++ b/app/slashcommands/helper_test.go @@ -45,10 +45,7 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo panic(err) } - memoryStore, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{IgnoreEnvironmentOverrides: true}) - if err != nil { - panic("failed to initialize memory store: " + err.Error()) - } + memoryStore := config.NewTestMemoryStore() config := memoryStore.Get() if configSet != nil { diff --git a/cmd/mattermost/commands/config.go b/cmd/mattermost/commands/config.go index 7c8f482d52..19733a3f1d 100644 --- a/cmd/mattermost/commands/config.go +++ b/cmd/mattermost/commands/config.go @@ -11,7 +11,6 @@ import ( "strconv" "strings" - "github.com/mattermost/viper" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -141,14 +140,12 @@ func configSubpathCmdF(command *cobra.Command, args []string) error { return nil } -func getConfigStore(command *cobra.Command) (config.Store, error) { +func getConfigStore(command *cobra.Command) (*config.Store, error) { if err := utils.TranslationsPreInit(); err != nil { return nil, errors.Wrap(err, "failed to initialize i18n") } - configDSN := viper.GetString("config") - - configStore, err := config.NewStore(configDSN, false) + configStore, err := config.NewStore(getConfigDSN(command, config.GetEnvironment()), false) if err != nil { return nil, errors.Wrap(err, "failed to initialize config store") } diff --git a/cmd/mattermost/commands/config_test.go b/cmd/mattermost/commands/config_test.go index a629116f02..f82d0cd5f4 100644 --- a/cmd/mattermost/commands/config_test.go +++ b/cmd/mattermost/commands/config_test.go @@ -94,6 +94,7 @@ func TestConfigValidate(t *testing.T) { tempFile, err := ioutil.TempFile("", "TestConfigValidate") require.NoError(t, err) defer os.Remove(tempFile.Name()) + tempFile.Write([]byte("{")) assert.Error(t, th.RunCommand(t, "--config", tempFile.Name(), "config", "validate")) th.CheckCommand(t, "config", "validate") diff --git a/cmd/mattermost/commands/init.go b/cmd/mattermost/commands/init.go index 63b53067df..689b255f2b 100644 --- a/cmd/mattermost/commands/init.go +++ b/cmd/mattermost/commands/init.go @@ -5,16 +5,14 @@ package commands import ( "github.com/mattermost/mattermost-server/v5/app" + "github.com/mattermost/mattermost-server/v5/config" "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/utils" - "github.com/mattermost/viper" "github.com/spf13/cobra" ) func InitDBCommandContextCobra(command *cobra.Command) (*app.App, error) { - config := viper.GetString("config") - - a, err := InitDBCommandContext(config) + a, err := InitDBCommandContext(getConfigDSN(command, config.GetEnvironment())) if err != nil { // Returning an error just prints the usage message, so actually panic diff --git a/cmd/mattermost/commands/jobserver.go b/cmd/mattermost/commands/jobserver.go index aef43d75cf..1bb56284e5 100644 --- a/cmd/mattermost/commands/jobserver.go +++ b/cmd/mattermost/commands/jobserver.go @@ -9,8 +9,8 @@ import ( "syscall" "github.com/mattermost/mattermost-server/v5/audit" + "github.com/mattermost/mattermost-server/v5/config" "github.com/mattermost/mattermost-server/v5/mlog" - "github.com/mattermost/viper" "github.com/spf13/cobra" ) @@ -32,10 +32,8 @@ func jobserverCmdF(command *cobra.Command, args []string) error { noJobs, _ := command.Flags().GetBool("nojobs") noSchedule, _ := command.Flags().GetBool("noschedule") - config := viper.GetString("config") - // Initialize - a, err := InitDBCommandContext(config) + a, err := InitDBCommandContext(getConfigDSN(command, config.GetEnvironment())) if err != nil { return err } diff --git a/cmd/mattermost/commands/plugin_test.go b/cmd/mattermost/commands/plugin_test.go index 71760eb79a..025415e89f 100644 --- a/cmd/mattermost/commands/plugin_test.go +++ b/cmd/mattermost/commands/plugin_test.go @@ -37,17 +37,21 @@ func TestPlugin(t *testing.T) { fs, err := config.NewFileStore(th.ConfigPath(), false) require.Nil(t, err) - require.NotNil(t, fs.Get().PluginSettings.PluginStates["testplugin"]) - assert.True(t, fs.Get().PluginSettings.PluginStates["testplugin"].Enable) - fs.Close() + cfsStore, err := config.NewStoreFromBacking(fs) + require.Nil(t, err) + require.NotNil(t, cfsStore.Get().PluginSettings.PluginStates["testplugin"]) + assert.True(t, cfsStore.Get().PluginSettings.PluginStates["testplugin"].Enable) + cfsStore.Close() output = th.CheckCommand(t, "plugin", "disable", "testplugin") assert.Contains(t, output, "Disabled plugin: testplugin") fs, err = config.NewFileStore(th.ConfigPath(), false) require.Nil(t, err) - require.NotNil(t, fs.Get().PluginSettings.PluginStates["testplugin"]) - assert.False(t, fs.Get().PluginSettings.PluginStates["testplugin"].Enable) - fs.Close() + cfsStore, err = config.NewStoreFromBacking(fs) + require.Nil(t, err) + require.NotNil(t, cfsStore.Get().PluginSettings.PluginStates["testplugin"]) + assert.False(t, cfsStore.Get().PluginSettings.PluginStates["testplugin"].Enable) + cfsStore.Close() th.CheckCommand(t, "plugin", "list") diff --git a/cmd/mattermost/commands/root.go b/cmd/mattermost/commands/root.go index 30e9432ac8..e2f83e3069 100644 --- a/cmd/mattermost/commands/root.go +++ b/cmd/mattermost/commands/root.go @@ -4,7 +4,6 @@ package commands import ( - "github.com/mattermost/viper" "github.com/spf13/cobra" ) @@ -22,12 +21,8 @@ var RootCmd = &cobra.Command{ } func init() { - RootCmd.PersistentFlags().StringP("config", "c", "config.json", "Configuration file to use.") + RootCmd.PersistentFlags().StringP("config", "c", "", "Configuration file to use.") RootCmd.PersistentFlags().Bool("disableconfigwatch", false, "When set config.json will not be loaded from disk when the file is changed.") RootCmd.PersistentFlags().Bool("platform", false, "This flag signifies that the user tried to start the command from the platform binary, so we can log a mssage") RootCmd.PersistentFlags().MarkHidden("platform") - - viper.SetEnvPrefix("mm") - viper.BindEnv("config") - viper.BindPFlag("config", RootCmd.PersistentFlags().Lookup("config")) } diff --git a/cmd/mattermost/commands/server.go b/cmd/mattermost/commands/server.go index 34ffd2ae93..9d5f6e39db 100644 --- a/cmd/mattermost/commands/server.go +++ b/cmd/mattermost/commands/server.go @@ -18,7 +18,6 @@ import ( "github.com/mattermost/mattermost-server/v5/utils" "github.com/mattermost/mattermost-server/v5/web" "github.com/mattermost/mattermost-server/v5/wsapi" - "github.com/mattermost/viper" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -36,8 +35,6 @@ func init() { } func serverCmdF(command *cobra.Command, args []string) error { - configDSN := viper.GetString("config") - disableConfigWatch, _ := command.Flags().GetBool("disableconfigwatch") usedPlatform, _ := command.Flags().GetBool("platform") @@ -46,7 +43,7 @@ func serverCmdF(command *cobra.Command, args []string) error { if err := utils.TranslationsPreInit(); err != nil { return errors.Wrapf(err, "unable to load Mattermost translation files") } - configStore, err := config.NewStore(configDSN, !disableConfigWatch) + configStore, err := config.NewStore(getConfigDSN(command, config.GetEnvironment()), !disableConfigWatch) if err != nil { return errors.Wrap(err, "failed to load configuration") } @@ -54,7 +51,7 @@ func serverCmdF(command *cobra.Command, args []string) error { return runServer(configStore, disableConfigWatch, usedPlatform, interruptChan) } -func runServer(configStore config.Store, disableConfigWatch bool, usedPlatform bool, interruptChan chan os.Signal) error { +func runServer(configStore *config.Store, disableConfigWatch bool, usedPlatform bool, interruptChan chan os.Signal) error { // Setting the highest traceback level from the code. // This is done to print goroutines from all threads (see golang.org/issue/13161) // and also preserve a crash dump for later investigation. diff --git a/cmd/mattermost/commands/server_test.go b/cmd/mattermost/commands/server_test.go index 98f9882244..368f5ca36a 100644 --- a/cmd/mattermost/commands/server_test.go +++ b/cmd/mattermost/commands/server_test.go @@ -57,13 +57,12 @@ func TestRunServerSuccess(t *testing.T) { th := SetupServerTest(t) defer th.TearDownServerTest() - configStore, err := config.NewMemoryStore() - require.NoError(t, err) + configStore := config.NewTestMemoryStore() // Use non-default listening port in case another server instance is already running. *configStore.Get().ServiceSettings.ListenAddress = UnitTestListeningPort - err = runServer(configStore, th.disableConfigWatch, false, th.interruptChan) + err := runServer(configStore, th.disableConfigWatch, false, th.interruptChan) require.NoError(t, err) } @@ -108,8 +107,7 @@ func TestRunServerSystemdNotification(t *testing.T) { ch <- string(data) }(socketReader) - configStore, err := config.NewMemoryStore() - require.NoError(t, err) + configStore := config.NewTestMemoryStore() // Use non-default listening port in case another server instance is already running. *configStore.Get().ServiceSettings.ListenAddress = UnitTestListeningPort @@ -132,12 +130,11 @@ func TestRunServerNoSystemd(t *testing.T) { os.Unsetenv("NOTIFY_SOCKET") defer os.Setenv("NOTIFY_SOCKET", originalSocket) - configStore, err := config.NewMemoryStore() - require.NoError(t, err) + configStore := config.NewTestMemoryStore() // Use non-default listening port in case another server instance is already running. *configStore.Get().ServiceSettings.ListenAddress = UnitTestListeningPort - err = runServer(configStore, th.disableConfigWatch, false, th.interruptChan) + err := runServer(configStore, th.disableConfigWatch, false, th.interruptChan) require.NoError(t, err) } diff --git a/cmd/mattermost/commands/utils.go b/cmd/mattermost/commands/utils.go index 0db282065a..c5d3e8bdf1 100644 --- a/cmd/mattermost/commands/utils.go +++ b/cmd/mattermost/commands/utils.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/mattermost/mattermost-server/v5/mlog" + "github.com/spf13/cobra" ) // prettyPrintStruct will return a prettyPrint version of a given struct @@ -99,3 +100,19 @@ func printStringMap(value reflect.Value, tabVal int) string { return out.String() } + +func getConfigDSN(command *cobra.Command, env map[string]string) string { + configDSN, _ := command.Flags().GetString("config") + + // Config not supplied in flag, check env + if configDSN == "" { + configDSN = env["MM_CONFIG"] + } + + // Config not supplied in env or flag use default + if configDSN == "" { + configDSN = "config.json" + } + + return configDSN +} diff --git a/config/common.go b/config/common.go deleted file mode 100644 index 40df705a6a..0000000000 --- a/config/common.go +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package config - -import ( - "bytes" - "io" - "strings" - "sync" - - "github.com/mattermost/mattermost-server/v5/model" - "github.com/pkg/errors" -) - -// commonStore enables code sharing between different backing implementations -type commonStore struct { - emitter - - configLock sync.RWMutex - config *model.Config - configWithoutOverrides *model.Config - environmentOverrides map[string]interface{} -} - -// Get fetches the current, cached configuration. -func (cs *commonStore) Get() *model.Config { - cs.configLock.RLock() - defer cs.configLock.RUnlock() - - return cs.config -} - -// GetEnvironmentOverrides fetches the configuration fields overridden by environment variables. -func (cs *commonStore) GetEnvironmentOverrides() map[string]interface{} { - cs.configLock.RLock() - defer cs.configLock.RUnlock() - - return cs.environmentOverrides -} - -// set replaces the current configuration in its entirety, and updates the backing store -// using the persist function argument. -// -// This function assumes no lock has been acquired, as it acquires a write lock itself. -func (cs *commonStore) set(newCfg *model.Config, allowEnvironmentOverrides bool, validate func(*model.Config) error, persist func(*model.Config) error) (*model.Config, error) { - cs.configLock.Lock() - var unlockOnce sync.Once - defer unlockOnce.Do(cs.configLock.Unlock) - - oldCfg := cs.config - - // TODO: disallow attempting to save a directly modified config (comparing pointers). This - // wouldn't be an exhaustive check, given the use of pointers throughout the data - // structure, but might prevent common mistakes. Requires upstream changes first. - // if newCfg == oldCfg { - // return nil, errors.New("old configuration modified instead of cloning") - // } - - // To both clone and re-apply the environment variable overrides we marshal and then - // unmarshal the config again. - var err error - newCfg, _, err = unmarshalConfig(strings.NewReader(newCfg.ToJson()), allowEnvironmentOverrides) - if err != nil { - return nil, errors.Wrapf(err, "failed to unmarshal config with env overrides") - } - - newCfg.SetDefaults() - - // Sometimes the config is received with "fake" data in sensitive fields. Apply the real - // data from the existing config as necessary. - desanitize(oldCfg, newCfg) - - if validate != nil { - if err := validate(newCfg); err != nil { - return nil, errors.Wrap(err, "new configuration is invalid") - } - } - - if err := persist(cs.RemoveNonPersistable(newCfg)); err != nil { - return nil, errors.Wrap(err, "failed to persist") - } - - cs.config = newCfg - - unlockOnce.Do(cs.configLock.Unlock) - - // Notify listeners synchronously. Ideally, this would be asynchronous, but existing code - // assumes this and there would be increased complexity to avoid racing updates. - cs.invokeConfigListeners(oldCfg, newCfg) - - return oldCfg, nil -} - -// load updates the current configuration from the given io.ReadCloser. -// -// This function assumes no lock has been acquired, as it acquires a write lock itself. -func (cs *commonStore) load(f io.ReadCloser, needsSave bool, validate func(*model.Config) error, persist func(*model.Config) error) error { - // Duplicate f so that we can read a configuration without applying environment overrides - f2 := new(bytes.Buffer) - tee := io.TeeReader(f, f2) - - allowEnvironmentOverrides := true - loadedCfg, environmentOverrides, err := unmarshalConfig(tee, allowEnvironmentOverrides) - if err != nil { - return errors.Wrapf(err, "failed to unmarshal config with env overrides") - } - - // Keep track of the original values that the Environment settings overrode - loadedCfgWithoutEnvOverrides, _, err := unmarshalConfig(f2, false) - if err != nil { - return errors.Wrapf(err, "failed to unmarshal config without env overrides") - } - - // SetDefaults generates various keys and salts if not previously configured. Determine if - // such a change will be made before invoking. - needsSave = needsSave || loadedCfg.SqlSettings.AtRestEncryptKey == nil || len(*loadedCfg.SqlSettings.AtRestEncryptKey) == 0 - needsSave = needsSave || loadedCfg.FileSettings.PublicLinkSalt == nil || len(*loadedCfg.FileSettings.PublicLinkSalt) == 0 - - loadedCfg.SetDefaults() - loadedCfgWithoutEnvOverrides.SetDefaults() - - if validate != nil { - if err = validate(loadedCfg); err != nil { - return errors.Wrap(err, "invalid config") - } - } - - if changed := fixConfig(loadedCfg); changed { - needsSave = true - } - - cs.configLock.Lock() - var unlockOnce sync.Once - defer unlockOnce.Do(cs.configLock.Unlock) - - if needsSave && persist != nil { - cfgWithoutEnvOverrides := removeEnvOverrides(loadedCfg, loadedCfgWithoutEnvOverrides, environmentOverrides) - if err = persist(cfgWithoutEnvOverrides); err != nil { - return errors.Wrap(err, "failed to persist required changes after load") - } - } - - oldCfg := cs.config - cs.config = loadedCfg - cs.configWithoutOverrides = loadedCfgWithoutEnvOverrides - cs.environmentOverrides = environmentOverrides - - unlockOnce.Do(cs.configLock.Unlock) - - // Notify listeners synchronously. Ideally, this would be asynchronous, but existing code - // assumes this and there would be increased complexity to avoid racing updates. - cs.invokeConfigListeners(oldCfg, loadedCfg) - - return nil -} - -// validate checks if the given configuration is valid -func (cs *commonStore) validate(cfg *model.Config) error { - if err := cfg.IsValid(); err != nil { - return err - } - - return nil -} - -// RemoveEnvironmentOverrides returns a new config without the given environment overrides. -func (cs *commonStore) RemoveEnvironmentOverrides(cfg *model.Config) *model.Config { - return removeEnvOverrides(cfg, cs.configWithoutOverrides, cs.environmentOverrides) -} - -// RemoveNonPersistable removes any aspect of the configuration we do not want to persist -func (cs *commonStore) RemoveNonPersistable(cfg *model.Config) *model.Config { - newCfg := cs.RemoveEnvironmentOverrides(cfg) - newCfg.FeatureFlags = nil - return newCfg -} diff --git a/config/common_test.go b/config/common_test.go index af5c6bd9ba..c2ea63a812 100644 --- a/config/common_test.go +++ b/config/common_test.go @@ -76,61 +76,63 @@ func init() { func TestMergeConfigs(t *testing.T) { t.Run("merge two default configs with different salts/keys", func(t *testing.T) { - base, err := config.NewMemoryStore() - require.NoError(t, err) - patch, err := config.NewMemoryStore() + base := &model.Config{} + base.SetDefaults() + patch := &model.Config{} + patch.SetDefaults() + + merged, err := config.Merge(base, patch, nil) require.NoError(t, err) - merged, err := config.Merge(base.Get(), patch.Get(), nil) - require.NoError(t, err) - - assert.Equal(t, patch.Get(), merged) + assert.Equal(t, patch, merged) }) t.Run("merge identical configs", func(t *testing.T) { - base, err := config.NewMemoryStore() - require.NoError(t, err) - patch := base.Get().Clone() + base := &model.Config{} + base.SetDefaults() + patch := base.Clone() - merged, err := config.Merge(base.Get(), patch, nil) + merged, err := config.Merge(base, patch, nil) require.NoError(t, err) - assert.Equal(t, base.Get(), merged) + assert.Equal(t, base, merged) assert.Equal(t, patch, merged) }) t.Run("merge configs with a different setting", func(t *testing.T) { - base, err := config.NewMemoryStore() - require.NoError(t, err) - patch := base.Get().Clone() + base := &model.Config{} + base.SetDefaults() + patch := base.Clone() patch.ServiceSettings.SiteURL = newString("http://newhost.ca") - merged, err := config.Merge(base.Get(), patch, nil) + merged, err := config.Merge(base, patch, nil) require.NoError(t, err) - assert.NotEqual(t, base.Get(), merged) + assert.NotEqual(t, base, merged) assert.Equal(t, patch, merged) }) t.Run("merge default config with changes from a mostly nil patch", func(t *testing.T) { - base, err := config.NewMemoryStore() - require.NoError(t, err) + base := &model.Config{} + base.SetDefaults() patch := &model.Config{} patch.ServiceSettings.SiteURL = newString("http://newhost.ca") patch.GoogleSettings.Enable = newBool(true) - expected := base.Get().Clone() + expected := base.Clone() expected.ServiceSettings.SiteURL = newString("http://newhost.ca") expected.GoogleSettings.Enable = newBool(true) - merged, err := config.Merge(base.Get(), patch, nil) + merged, err := config.Merge(base, patch, nil) require.NoError(t, err) - assert.NotEqual(t, base.Get(), merged) + assert.NotEqual(t, base, merged) assert.NotEqual(t, patch, merged) assert.Equal(t, expected, merged) }) } func TestConfigEnvironmentOverrides(t *testing.T) { - base, err := config.NewMemoryStore() + memstore, err := config.NewMemoryStore() + require.NoError(t, err) + base, err := config.NewStoreFromBacking(memstore) require.NoError(t, err) originalConfig := &model.Config{} originalConfig.ServiceSettings.SiteURL = newString("http://notoverriden.ca") @@ -157,7 +159,9 @@ func TestRemoveEnvironmentOverrides(t *testing.T) { os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://overridden.ca") defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") - base, err := config.NewMemoryStore() + memstore, err := config.NewMemoryStore() + require.NoError(t, err) + base, err := config.NewStoreFromBacking(memstore) require.NoError(t, err) oldCfg := base.Get() assert.Equal(t, "http://overridden.ca", *oldCfg.ServiceSettings.SiteURL) @@ -165,22 +169,5 @@ func TestRemoveEnvironmentOverrides(t *testing.T) { assert.Equal(t, "", *newCfg.ServiceSettings.SiteURL) } -func TestRemoveNonPersistable(t *testing.T) { - os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://overridden.ca") - defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") - - base, err := config.NewMemoryStore() - require.NoError(t, err) - oldCfg := base.Get() - assert.Equal(t, "http://overridden.ca", *oldCfg.ServiceSettings.SiteURL) - oldCfg.FeatureFlags = &model.FeatureFlags{ - TestFeature: "teststring", - } - - newCfg := base.RemoveNonPersistable(oldCfg) - assert.Equal(t, "", *newCfg.ServiceSettings.SiteURL) - assert.Nil(t, newCfg.FeatureFlags) -} - func newBool(b bool) *bool { return &b } func newString(s string) *string { return &s } diff --git a/config/database.go b/config/database.go index e07fd7fe37..ba288b1386 100644 --- a/config/database.go +++ b/config/database.go @@ -6,7 +6,7 @@ package config import ( "bytes" "database/sql" - "io/ioutil" + "encoding/json" "strings" "github.com/jmoiron/sqlx" @@ -28,9 +28,8 @@ import ( const MaxWriteLength = 4 * 1024 * 1024 // DatabaseStore is a config store backed by a database. +// Not to be used directly. Only to be used as a backing store for config.Store type DatabaseStore struct { - commonStore - originalDsn string driverName string dataSourceName string @@ -59,10 +58,6 @@ func NewDatabaseStore(dsn string) (ds *DatabaseStore, err error) { return nil, errors.Wrap(err, "failed to initialize") } - if err = ds.Load(); err != nil { - return nil, errors.Wrap(err, "failed to load") - } - return ds, nil } @@ -159,8 +154,8 @@ func parseDSN(dsn string) (string, string, error) { } // Set replaces the current configuration in its entirety and updates the backing store. -func (ds *DatabaseStore) Set(newCfg *model.Config) (*model.Config, error) { - return ds.commonStore.set(newCfg, true, ds.commonStore.validate, ds.persist) +func (ds *DatabaseStore) Set(newCfg *model.Config) error { + return ds.persist(newCfg) } // maxLength identifies the maximum length of a configuration or configuration file @@ -232,35 +227,23 @@ func (ds *DatabaseStore) persist(cfg *model.Config) error { } // Load updates the current configuration from the backing store. -func (ds *DatabaseStore) Load() (err error) { - var needsSave bool +func (ds *DatabaseStore) Load() ([]byte, error) { var configurationData []byte row := ds.db.QueryRow("SELECT Value FROM Configurations WHERE Active") - if err = row.Scan(&configurationData); err != nil && err != sql.ErrNoRows { - return errors.Wrap(err, "failed to query active configuration") + if err := row.Scan(&configurationData); err != nil && err != sql.ErrNoRows { + return nil, errors.Wrap(err, "failed to query active configuration") } // Initialize from the default config if no active configuration could be found. if len(configurationData) == 0 { - needsSave = true - - defaultCfg := &model.Config{} - defaultCfg.SetDefaults() - - // Assume the database storing the config is also to be used for the application. - // This can be overridden using environment variables on first start if necessary, - // or changed from the system console afterwards. - *defaultCfg.SqlSettings.DriverName = ds.driverName - *defaultCfg.SqlSettings.DataSource = ds.dataSourceName - - configurationData, err = marshalConfig(defaultCfg) - if err != nil { - return errors.Wrap(err, "failed to serialize default config") - } + configWithDB := model.Config{} + configWithDB.SqlSettings.DriverName = model.NewString(ds.driverName) + configWithDB.SqlSettings.DataSource = model.NewString(ds.dataSourceName) + return json.Marshal(configWithDB) } - return ds.commonStore.load(ioutil.NopCloser(bytes.NewReader(configurationData)), needsSave, ds.commonStore.validate, ds.persist) + return configurationData, nil } // GetFile fetches the contents of a previously persisted configuration file. @@ -287,7 +270,6 @@ func (ds *DatabaseStore) SetFile(name string, data []byte) error { if err != nil { return errors.Wrap(err, "file data failed length check") } - params := map[string]interface{}{ "name": name, "data": data, @@ -352,8 +334,10 @@ func (ds *DatabaseStore) String() string { // Close cleans up resources associated with the store. func (ds *DatabaseStore) Close() error { - ds.configLock.Lock() - defer ds.configLock.Unlock() - return ds.db.Close() } + +// Watch nothing on memory store +func (ds *DatabaseStore) Watch(_ func()) error { + return nil +} diff --git a/config/database_test.go b/config/database_test.go index 132ac5d326..1a1db3bbfd 100644 --- a/config/database_test.go +++ b/config/database_test.go @@ -5,6 +5,7 @@ package config_test import ( "bytes" + "encoding/json" "fmt" "os" "strings" @@ -79,7 +80,8 @@ func getActualDatabaseConfig(t *testing.T) (string, *model.Config) { err := db.Get(&actual, "SELECT Id, Value FROM Configurations WHERE Active") require.NoError(t, err) - actualCfg, _, err := config.UnmarshalConfig(bytes.NewReader(actual.Value), false) + var actualCfg *model.Config + err = json.Unmarshal(actual.Value, &actualCfg) require.Nil(t, err) return actual.ID, actualCfg } @@ -91,7 +93,8 @@ func getActualDatabaseConfig(t *testing.T) (string, *model.Config) { err := db.Get(&actual, "SELECT Id, Value FROM Configurations WHERE Active") require.NoError(t, err) - actualCfg, _, err := config.UnmarshalConfig(bytes.NewReader(actual.Value), false) + var actualCfg *model.Config + err = json.Unmarshal(actual.Value, &actualCfg) require.Nil(t, err) return actual.ID, actualCfg } @@ -112,6 +115,17 @@ func assertDatabaseNotEqualsConfig(t *testing.T, expectedCfg *model.Config) { assert.NotEqual(t, expectedCfg, actualCfg) } +func newTestDatabaseStore(t *testing.T) (*config.Store, error) { + sqlSettings := mainHelper.GetSQLSettings() + dss, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + require.NoError(t, err) + + cStore, err := config.NewStoreFromBacking(dss) + require.NoError(t, err) + + return cStore, nil +} + func TestDatabaseStoreNew(t *testing.T) { if testing.Short() { t.SkipNow() @@ -119,7 +133,7 @@ func TestDatabaseStoreNew(t *testing.T) { sqlSettings := mainHelper.GetSQLSettings() t.Run("no existing configuration - initialization required", func(t *testing.T) { - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -130,7 +144,7 @@ func TestDatabaseStoreNew(t *testing.T) { _, tearDown := setupConfigDatabase(t, testConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -142,7 +156,7 @@ func TestDatabaseStoreNew(t *testing.T) { _, tearDown := setupConfigDatabase(t, minimalConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -173,8 +187,7 @@ func TestDatabaseStoreGet(t *testing.T) { _, tearDown := setupConfigDatabase(t, testConfig, nil) defer tearDown() - sqlSettings := mainHelper.GetSQLSettings() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -185,13 +198,6 @@ func TestDatabaseStoreGet(t *testing.T) { assert.Equal(t, "http://TestStoreNew", *cfg.ServiceSettings.SiteURL) assert.True(t, cfg == cfg2, "Get() returned different configuration instances") - - newCfg := &model.Config{} - oldCfg, err := ds.Set(newCfg) - require.NoError(t, err) - - assert.True(t, oldCfg == cfg, "returned config after set() changed original") - assert.False(t, newCfg == cfg, "returned config should have been different from original") } func TestDatabaseStoreGetEnivironmentOverrides(t *testing.T) { @@ -199,8 +205,7 @@ func TestDatabaseStoreGetEnivironmentOverrides(t *testing.T) { _, tearDown := setupConfigDatabase(t, testConfig, nil) defer tearDown() - sqlSettings := mainHelper.GetSQLSettings() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -210,7 +215,7 @@ func TestDatabaseStoreGetEnivironmentOverrides(t *testing.T) { os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override") defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") - ds, err = config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err = newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -222,8 +227,7 @@ func TestDatabaseStoreGetEnivironmentOverrides(t *testing.T) { _, tearDown := setupConfigDatabase(t, testConfig, nil) defer tearDown() - sqlSettings := mainHelper.GetSQLSettings() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -233,7 +237,7 @@ func TestDatabaseStoreGetEnivironmentOverrides(t *testing.T) { os.Setenv("MM_PLUGINSETTINGS_ENABLEUPLOADS", "true") defer os.Unsetenv("MM_PLUGINSETTINGS_ENABLEUPLOADS") - ds, err = config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err = newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -245,8 +249,7 @@ func TestDatabaseStoreGetEnivironmentOverrides(t *testing.T) { _, tearDown := setupConfigDatabase(t, testConfig, nil) defer tearDown() - sqlSettings := mainHelper.GetSQLSettings() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -256,7 +259,7 @@ func TestDatabaseStoreGetEnivironmentOverrides(t *testing.T) { os.Setenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM", "3000") defer os.Unsetenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM") - ds, err = config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err = newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -268,8 +271,7 @@ func TestDatabaseStoreGetEnivironmentOverrides(t *testing.T) { _, tearDown := setupConfigDatabase(t, testConfig, nil) defer tearDown() - sqlSettings := mainHelper.GetSQLSettings() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -279,7 +281,7 @@ func TestDatabaseStoreGetEnivironmentOverrides(t *testing.T) { os.Setenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE", "123456") defer os.Unsetenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE") - ds, err = config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err = newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -291,8 +293,7 @@ func TestDatabaseStoreGetEnivironmentOverrides(t *testing.T) { _, tearDown := setupConfigDatabase(t, testConfig, nil) defer tearDown() - sqlSettings := mainHelper.GetSQLSettings() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -302,7 +303,7 @@ func TestDatabaseStoreGetEnivironmentOverrides(t *testing.T) { os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db") defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS") - ds, err = config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err = newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -317,8 +318,7 @@ func TestDatabaseStoreGetEnivironmentOverrides(t *testing.T) { _, tearDown := setupConfigDatabase(t, testConfig, nil) defer tearDown() - sqlSettings := mainHelper.GetSQLSettings() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -328,7 +328,7 @@ func TestDatabaseStoreGetEnivironmentOverrides(t *testing.T) { os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db user:pwd@db2:5433/test-db2 user:pwd@db3:5434/test-db3") defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS") - ds, err = config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err = newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -341,7 +341,6 @@ func TestDatabaseStoreSet(t *testing.T) { if testing.Short() { t.SkipNow() } - sqlSettings := mainHelper.GetSQLSettings() t.Run("set same pointer value", func(t *testing.T) { t.Skip("not yet implemented") @@ -349,7 +348,7 @@ func TestDatabaseStoreSet(t *testing.T) { _, tearDown := setupConfigDatabase(t, emptyConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -363,17 +362,14 @@ func TestDatabaseStoreSet(t *testing.T) { _, tearDown := setupConfigDatabase(t, minimalConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() - oldCfg := ds.Get() - newCfg := &model.Config{} - retCfg, err := ds.Set(newCfg) + _, err = ds.Set(newCfg) require.NoError(t, err) - assert.Equal(t, oldCfg, retCfg) assert.Equal(t, "", *ds.Get().ServiceSettings.SiteURL) }) @@ -382,18 +378,15 @@ func TestDatabaseStoreSet(t *testing.T) { _, tearDown := setupConfigDatabase(t, ldapConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() - oldCfg := ds.Get() - newCfg := &model.Config{} newCfg.LdapSettings.BindPassword = sToP(model.FAKE_SETTING) - retCfg, err := ds.Set(newCfg) + _, err = ds.Set(newCfg) require.NoError(t, err) - assert.Equal(t, oldCfg, retCfg) assert.Equal(t, "password", *ds.Get().LdapSettings.BindPassword) }) @@ -402,7 +395,7 @@ func TestDatabaseStoreSet(t *testing.T) { _, tearDown := setupConfigDatabase(t, emptyConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -421,7 +414,7 @@ func TestDatabaseStoreSet(t *testing.T) { _, tearDown := setupConfigDatabase(t, minimalConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -440,7 +433,7 @@ func TestDatabaseStoreSet(t *testing.T) { _, tearDown := setupConfigDatabase(t, readOnlyConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -460,7 +453,7 @@ func TestDatabaseStoreSet(t *testing.T) { _, tearDown := setupConfigDatabase(t, minimalConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -483,10 +476,11 @@ func TestDatabaseStoreSet(t *testing.T) { _, tearDown := setupConfigDatabase(t, emptyConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() + sqlSettings := mainHelper.GetSQLSettings() db := sqlx.NewDb(mainHelper.GetSQLSupplier().GetMaster().Db, *sqlSettings.DriverName) _, err = db.Exec("DROP TABLE Configurations") require.NoError(t, err) @@ -507,7 +501,7 @@ func TestDatabaseStoreSet(t *testing.T) { _, tearDown := setupConfigDatabase(t, emptyConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -524,12 +518,10 @@ func TestDatabaseStoreSet(t *testing.T) { activeID, tearDown := setupConfigDatabase(t, emptyConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() - oldCfg := ds.Get() - called := make(chan bool, 1) callback := func(oldfg, newCfg *model.Config) { called <- true @@ -538,9 +530,8 @@ func TestDatabaseStoreSet(t *testing.T) { newCfg := &model.Config{} - retCfg, err := ds.Set(newCfg) + _, err = ds.Set(newCfg) require.NoError(t, err) - assert.Equal(t, oldCfg, retCfg) id, _ := getActualDatabaseConfig(t) assert.NotEqual(t, activeID, id, "new record should have been written") @@ -553,13 +544,12 @@ func TestDatabaseStoreLoad(t *testing.T) { if testing.Short() { t.SkipNow() } - sqlSettings := mainHelper.GetSQLSettings() t.Run("active configuration no longer exists", func(t *testing.T) { _, tearDown := setupConfigDatabase(t, emptyConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -574,7 +564,7 @@ func TestDatabaseStoreLoad(t *testing.T) { _, tearDown := setupConfigDatabase(t, minimalConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -596,7 +586,7 @@ func TestDatabaseStoreLoad(t *testing.T) { os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://overridePersistEnvVariables") defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -617,7 +607,7 @@ func TestDatabaseStoreLoad(t *testing.T) { os.Setenv("MM_PLUGINSETTINGS_ENABLEUPLOADS", "true") defer os.Unsetenv("MM_PLUGINSETTINGS_ENABLEUPLOADS") - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -640,7 +630,7 @@ func TestDatabaseStoreLoad(t *testing.T) { os.Setenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM", "3000") defer os.Unsetenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM") - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -663,7 +653,7 @@ func TestDatabaseStoreLoad(t *testing.T) { os.Setenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE", "123456") defer os.Unsetenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE") - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -686,7 +676,7 @@ func TestDatabaseStoreLoad(t *testing.T) { os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db") defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS") - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -711,7 +701,7 @@ func TestDatabaseStoreLoad(t *testing.T) { os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db") defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS") - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -731,13 +721,14 @@ func TestDatabaseStoreLoad(t *testing.T) { _, tearDown := setupConfigDatabase(t, emptyConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() cfgData, err := config.MarshalConfig(invalidConfig) require.NoError(t, err) + sqlSettings := mainHelper.GetSQLSettings() db := sqlx.NewDb(mainHelper.GetSQLSupplier().GetMaster().Db, *sqlSettings.DriverName) truncateTables(t) id := model.NewId() @@ -758,7 +749,7 @@ func TestDatabaseStoreLoad(t *testing.T) { _, tearDown := setupConfigDatabase(t, fixesRequiredConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -772,7 +763,7 @@ func TestDatabaseStoreLoad(t *testing.T) { _, tearDown := setupConfigDatabase(t, emptyConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -796,7 +787,7 @@ func TestDatabaseGetFile(t *testing.T) { }) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -827,7 +818,7 @@ func TestDatabaseSetFile(t *testing.T) { _, tearDown := setupConfigDatabase(t, minimalConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -880,7 +871,7 @@ func TestDatabaseHasFile(t *testing.T) { _, tearDown := setupConfigDatabase(t, minimalConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -893,7 +884,7 @@ func TestDatabaseHasFile(t *testing.T) { _, tearDown := setupConfigDatabase(t, minimalConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -911,7 +902,7 @@ func TestDatabaseHasFile(t *testing.T) { }) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -924,7 +915,7 @@ func TestDatabaseHasFile(t *testing.T) { _, tearDown := setupConfigDatabase(t, minimalConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -939,7 +930,7 @@ func TestDatabaseRemoveFile(t *testing.T) { _, tearDown := setupConfigDatabase(t, minimalConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -951,7 +942,7 @@ func TestDatabaseRemoveFile(t *testing.T) { _, tearDown := setupConfigDatabase(t, minimalConfig, nil) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -975,7 +966,7 @@ func TestDatabaseRemoveFile(t *testing.T) { }) defer tearDown() - ds, err := config.NewDatabaseStore(getDsn(*mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) defer ds.Close() @@ -998,8 +989,7 @@ func TestDatabaseStoreString(t *testing.T) { _, tearDown := setupConfigDatabase(t, emptyConfig, nil) defer tearDown() - sqlSettings := mainHelper.GetSQLSettings() - ds, err := config.NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) + ds, err := newTestDatabaseStore(t) require.NoError(t, err) require.NotNil(t, ds) defer ds.Close() diff --git a/config/environment.go b/config/environment.go index 8ce0b65836..092cee52ed 100644 --- a/config/environment.go +++ b/config/environment.go @@ -4,11 +4,122 @@ package config import ( + "os" "reflect" + "strconv" + "strings" "github.com/mattermost/mattermost-server/v5/model" ) +func GetEnvironment() map[string]string { + mmenv := make(map[string]string) + for _, env := range os.Environ() { + kv := strings.SplitN(env, "=", 2) + key := strings.ToUpper(kv[0]) + if strings.HasPrefix(key, "MM") { + mmenv[key] = kv[1] + } + } + + return mmenv +} + +func applyEnvKey(key, value string, rValueSubject reflect.Value) { + keyParts := strings.SplitN(key, "_", 2) + if len(keyParts) < 1 { + return + } + rFieldValue := rValueSubject.FieldByNameFunc(func(candidate string) bool { + candidateUpper := strings.ToUpper(candidate) + return candidateUpper == keyParts[0] + }) + + if !rFieldValue.IsValid() { + return + } + + if rFieldValue.Kind() == reflect.Ptr { + rFieldValue = rFieldValue.Elem() + if !rFieldValue.IsValid() { + return + } + } + + switch rFieldValue.Kind() { + case reflect.Struct: + // If we have only one part left, we can't deal with a struct + // the env var is incomplete so give up. + if len(keyParts) < 2 { + return + } + applyEnvKey(keyParts[1], value, rFieldValue) + case reflect.String: + rFieldValue.Set(reflect.ValueOf(value)) + case reflect.Bool: + boolVal, err := strconv.ParseBool(value) + if err == nil { + rFieldValue.Set(reflect.ValueOf(boolVal)) + } + case reflect.Int: + intVal, err := strconv.ParseInt(value, 10, 0) + if err == nil { + rFieldValue.Set(reflect.ValueOf(int(intVal))) + } + case reflect.Int64: + intVal, err := strconv.ParseInt(value, 10, 0) + if err == nil { + rFieldValue.Set(reflect.ValueOf(intVal)) + } + case reflect.SliceOf(reflect.TypeOf("")).Kind(): + rFieldValue.Set(reflect.ValueOf(strings.Split(value, " "))) + } +} + +func applyEnvironmentMap(inputConfig *model.Config, env map[string]string) *model.Config { + appliedConfig := inputConfig.Clone() + + rvalConfig := reflect.ValueOf(appliedConfig).Elem() + for envKey, envValue := range env { + applyEnvKey(strings.TrimPrefix(envKey, "MM_"), envValue, rvalConfig) + } + + return appliedConfig +} + +// generateEnvironmentMap creates a map[string]interface{} containing true at the leaves mirroring the +// configuration structure so the client can know which env variables are overridden +func generateEnvironmentMap(env map[string]string) map[string]interface{} { + rType := reflect.TypeOf(model.Config{}) + return generateEnvironmentMapWithBaseKey(env, rType, "MM") +} + +func generateEnvironmentMapWithBaseKey(env map[string]string, rType reflect.Type, base string) map[string]interface{} { + if rType.Kind() != reflect.Struct { + return nil + } + + mapRepresentation := make(map[string]interface{}) + for i := 0; i < rType.NumField(); i++ { + rField := rType.Field(i) + if rField.Type.Kind() == reflect.Struct { + if val := generateEnvironmentMapWithBaseKey(env, rField.Type, base+"_"+rField.Name); val != nil { + mapRepresentation[rField.Name] = val + } + } else { + if _, ok := env[strings.ToUpper(base+"_"+rField.Name)]; ok { + mapRepresentation[rField.Name] = true + } + } + } + + if len(mapRepresentation) == 0 { + return nil + } + + return mapRepresentation +} + // removeEnvOverrides returns a new config without the given environment overrides. // If a config variable has an environment override, that variable is set to the value that was // read from the store. diff --git a/config/environment_test.go b/config/environment_test.go index 4e464ac94b..213d30c256 100644 --- a/config/environment_test.go +++ b/config/environment_test.go @@ -10,28 +10,118 @@ import ( "github.com/stretchr/testify/require" ) +func modifiedDefault(modify func(*model.Config)) *model.Config { + def := defaultConfig() + modify(def) + return def +} + +func defaultConfig() *model.Config { + def := &model.Config{} + def.SetDefaults() + return def +} + func TestRemoveEnvOverrides(t *testing.T) { - defaultCfg := &model.Config{} - defaultCfg.SetDefaults() - - newCfg := defaultCfg.Clone() - newCfg.EmailSettings.EnableSignUpWithEmail = model.NewBool(false) - - envOverrides := map[string]interface{}{ - "EmailSettings": map[string]interface{}{ - "EnableSignUpWithEmail": false, + var tests = []struct { + name string + inputConfig *model.Config + env map[string]string + expectedConfig *model.Config + }{ + { + name: "basic override", + inputConfig: modifiedDefault(func(in *model.Config) { + *in.ServiceSettings.TLSMinVer = "1.4" + }), + env: map[string]string{ + "MM_SERVICESETTINGS_TLSMINVER": "1.5", + }, + expectedConfig: modifiedDefault(func(in *model.Config) { + *in.ServiceSettings.TLSMinVer = "1.5" + }), + }, + { + name: "feature flags", + inputConfig: modifiedDefault(func(in *model.Config) { + in.FeatureFlags.TestFeature = "somevalue" + }), + env: map[string]string{ + "MM_FEATUREFLAGS_TESTFEATURE": "correctvalue", + }, + expectedConfig: modifiedDefault(func(in *model.Config) { + in.FeatureFlags.TestFeature = "correctvalue" + }), + }, + { + name: "int setting", + inputConfig: modifiedDefault(func(in *model.Config) { + *in.ClusterSettings.GossipPort = 500 + }), + env: map[string]string{ + "MM_CLUSTERSETTINGS_GOSSIPPORT": "600", + }, + expectedConfig: modifiedDefault(func(in *model.Config) { + *in.ClusterSettings.GossipPort = 600 + }), + }, + { + name: "int64 setting", + inputConfig: modifiedDefault(func(in *model.Config) { + *in.ServiceSettings.TLSStrictTransportMaxAge = 500 + }), + env: map[string]string{ + "MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE": "4294967294", + }, + expectedConfig: modifiedDefault(func(in *model.Config) { + *in.ServiceSettings.TLSStrictTransportMaxAge = 4294967294 + }), + }, + { + name: "bool setting", + inputConfig: modifiedDefault(func(in *model.Config) { + *in.ClusterSettings.UseIpAddress = false + }), + env: map[string]string{ + "MM_CLUSTERSETTINGS_USEIPADDRESS": "true", + }, + expectedConfig: modifiedDefault(func(in *model.Config) { + *in.ClusterSettings.UseIpAddress = true + }), + }, + { + name: "[]string setting", + inputConfig: modifiedDefault(func(in *model.Config) { + in.SqlSettings.DataSourceReplicas = []string{"somthing"} + }), + env: map[string]string{ + "MM_SQLSETTINGS_DATASOURCEREPLICAS": "otherthing alsothis", + }, + expectedConfig: modifiedDefault(func(in *model.Config) { + in.SqlSettings.DataSourceReplicas = []string{"otherthing", "alsothis"} + }), + }, + { + name: "bad env", + inputConfig: modifiedDefault(func(in *model.Config) { + }), + env: map[string]string{ + "MM_SERVICESETTINGS": "huh?", + "NOTMM": "huh?", + "MM_NOTEXIST": "huh?", + "MM_NOTEXIST_MORE_AND_MORE": "huh?", + "MM_": "huh?", + "MM": "huh?", + "MM__": "huh?", + "_": "huh?", + }, + expectedConfig: modifiedDefault(func(in *model.Config) { + }), }, } - - updatedCfg := removeEnvOverrides(newCfg, defaultCfg, envOverrides) - require.NotNil(t, updatedCfg) - require.True(t, *updatedCfg.EmailSettings.EnableSignUpWithEmail) - - envOverrides["ServiceSettings"] = map[string]interface{}{ - "NonExistentConfig": true, + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + require.Equal(t, testCase.expectedConfig, applyEnvironmentMap(testCase.inputConfig, testCase.env)) + }) } - - require.NotPanics(t, func() { - _ = removeEnvOverrides(defaultCfg, defaultCfg, envOverrides) - }, "invalid setting should not panic") } diff --git a/config/export_test.go b/config/export_test.go index fbacab7c31..dbfff3e464 100644 --- a/config/export_test.go +++ b/config/export_test.go @@ -4,8 +4,6 @@ package config import ( - "io" - "github.com/jmoiron/sqlx" "github.com/mattermost/mattermost-server/v5/model" ) @@ -15,11 +13,6 @@ func MarshalConfig(cfg *model.Config) ([]byte, error) { return marshalConfig(cfg) } -// UnmarshalConfig exposes the internal unmarshalConfig to tests only. -func UnmarshalConfig(r io.Reader, allowEnvironmentOverrides bool) (*model.Config, map[string]interface{}, error) { - return unmarshalConfig(r, allowEnvironmentOverrides) -} - // InitializeConfigurationsTable exposes the internal initializeConfigurationsTable to test only. func InitializeConfigurationsTable(db *sqlx.DB) error { return initializeConfigurationsTable(db) diff --git a/config/feature_flags.go b/config/feature_flags.go new file mode 100644 index 0000000000..0dc07561a3 --- /dev/null +++ b/config/feature_flags.go @@ -0,0 +1,99 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package config + +import ( + "math" + "reflect" + + "github.com/mattermost/mattermost-server/v5/mlog" + "github.com/mattermost/mattermost-server/v5/model" + "github.com/pkg/errors" + "github.com/splitio/go-client/v6/splitio/client" + "github.com/splitio/go-client/v6/splitio/conf" +) + +type FeatureFlagSyncParams struct { + ServerID string + SplitKey string + SyncIntervalSeconds int + Log *mlog.Logger +} + +type FeatureFlagSynchronizer struct { + FeatureFlagSyncParams + + client *client.SplitClient + stop chan struct{} + stopped chan struct{} +} + +var featureNames = getStructFields(model.FeatureFlags{}) + +func NewFeatureFlagSynchronizer(params FeatureFlagSyncParams) (*FeatureFlagSynchronizer, error) { + cfg := conf.Default() + if params.Log != nil { + cfg.Logger = &splitLogger{wrappedLog: params.Log.With(mlog.String("service", "split"))} + } else { + cfg.LoggerConfig.LogLevel = math.MinInt32 + } + factory, err := client.NewSplitFactory(params.SplitKey, cfg) + if err != nil { + return nil, errors.Wrap(err, "unable to create split factory") + } + + return &FeatureFlagSynchronizer{ + FeatureFlagSyncParams: params, + client: factory.Client(), + stop: make(chan struct{}), + stopped: make(chan struct{}), + }, nil +} + +// ensureReady blocks until the syncronizer is ready to update feature flag values +func (f *FeatureFlagSynchronizer) EnsureReady() error { + if err := f.client.BlockUntilReady(10); err != nil { + return errors.Wrap(err, "split.io client could not initalize") + } + + return nil +} + +func (f *FeatureFlagSynchronizer) UpdateFeatureFlagValues(base model.FeatureFlags) model.FeatureFlags { + featuresMap := f.client.Treatments(f.ServerID, featureNames, nil) + ffm := featureFlagsFromMap(featuresMap, base) + return ffm +} + +func (f *FeatureFlagSynchronizer) Close() { + f.client.Destroy() +} + +// featureFlagsFromMap sets the feature flags from a map[string]string. +// It starts with baseFeatureFlags and only sets values that are +// given by the upstream management system. +// Makes the assumption that all feature flags are strings for now. +func featureFlagsFromMap(featuresMap map[string]string, baseFeatureFlags model.FeatureFlags) model.FeatureFlags { + refStruct := reflect.ValueOf(&baseFeatureFlags).Elem() + for fieldName, fieldValue := range featuresMap { + refField := refStruct.FieldByName(fieldName) + // "control" is returned by split.io if the treatment is not found, in this case we should use the default value. + if !refField.IsValid() || !refField.CanSet() || fieldValue == "control" { + continue + } + + refField.Set(reflect.ValueOf(fieldValue)) + } + return baseFeatureFlags +} + +func getStructFields(s interface{}) []string { + structType := reflect.TypeOf(s) + fieldNames := make([]string, 0, structType.NumField()) + for i := 0; i < structType.NumField(); i++ { + fieldNames = append(fieldNames, structType.Field(i).Name) + } + + return fieldNames +} diff --git a/config/feature_flags_test.go b/config/feature_flags_test.go new file mode 100644 index 0000000000..125a9cc9bb --- /dev/null +++ b/config/feature_flags_test.go @@ -0,0 +1,75 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package config + +import ( + "testing" + + "github.com/mattermost/mattermost-server/v5/model" + "github.com/stretchr/testify/require" +) + +func TestGetStructFields(t *testing.T) { + type testStruct struct { + FieldOne string + SecondField bool + SomeOtherField int + } + + fields := getStructFields(testStruct{}) + require.Equal(t, + []string{ + "FieldOne", + "SecondField", + "SomeOtherField", + }, + fields, + ) + + featureFlagsFields := getStructFields(model.FeatureFlags{}) + require.Contains(t, featureFlagsFields, "TestFeature") +} + +func TestFeatureFlagsFromMap(t *testing.T) { + for name, tc := range map[string]struct { + FeatureMap map[string]string + Base model.FeatureFlags + ExpectedTestValue string + }{ + "empty": { + FeatureMap: map[string]string{}, + Base: model.FeatureFlags{}, + ExpectedTestValue: "", + }, + "no base value": { + FeatureMap: map[string]string{"TestFeature": "expectedvalue"}, + Base: model.FeatureFlags{}, + ExpectedTestValue: "expectedvalue", + }, + "only base value": { + FeatureMap: map[string]string{}, + Base: model.FeatureFlags{TestFeature: "somebasevalue"}, + ExpectedTestValue: "somebasevalue", + }, + "override base value": { + FeatureMap: map[string]string{"TestFeature": "overridevalue"}, + Base: model.FeatureFlags{TestFeature: "somebasevalue"}, + ExpectedTestValue: "overridevalue", + }, + "override base value with extras": { + FeatureMap: map[string]string{"TestFeature": "overridevalue", "SomeOldFlag": "oldvalue"}, + Base: model.FeatureFlags{TestFeature: "somebasevalue"}, + ExpectedTestValue: "overridevalue", + }, + "all values do not exist": { + FeatureMap: map[string]string{"SomeOldFlag": "oldvalue"}, + Base: model.FeatureFlags{}, + ExpectedTestValue: "", + }, + } { + t.Run(name, func(t *testing.T) { + require.Equal(t, tc.ExpectedTestValue, featureFlagsFromMap(tc.FeatureMap, tc.Base).TestFeature) + }) + } +} diff --git a/config/file.go b/config/file.go index 919068821e..eb5dcda0c4 100644 --- a/config/file.go +++ b/config/file.go @@ -4,9 +4,7 @@ package config import ( - "bytes" "fmt" - "io" "io/ioutil" "os" "path/filepath" @@ -26,12 +24,12 @@ var ( // FileStore is a config store backed by a file such as config/config.json. // // It also uses the folder containing the configuration file for storing other configuration files. +// Not to be used directly. Only to be used as a backing store for config.Store type FileStore struct { - commonStore - - path string - watch bool - watcher *watcher + path string + watch bool + watcher *watcher + callback func() } // NewFileStore creates a new instance of a config store backed by the given file path. @@ -47,15 +45,6 @@ func NewFileStore(path string, watch bool) (fs *FileStore, err error) { path: resolvedPath, watch: watch, } - if err = fs.Load(); err != nil { - return nil, errors.Wrap(err, "failed to load") - } - - if fs.watch { - if err = fs.startWatcher(); err != nil { - mlog.Error("failed to start config watcher", mlog.String("path", path), mlog.Err(err)) - } - } return fs, nil } @@ -104,19 +93,21 @@ func (fs *FileStore) resolveFilePath(name string) string { } // Set replaces the current configuration in its entirety and updates the backing store. -func (fs *FileStore) Set(newCfg *model.Config) (*model.Config, error) { - return fs.commonStore.set(newCfg, true, func(cfg *model.Config) error { - if *fs.config.ClusterSettings.Enable && *fs.config.ClusterSettings.ReadOnlyConfig { - return ErrReadOnlyConfiguration - } +func (fs *FileStore) Set(newCfg *model.Config) error { + if *newCfg.ClusterSettings.Enable && *newCfg.ClusterSettings.ReadOnlyConfig { + return ErrReadOnlyConfiguration + } - return fs.commonStore.validate(cfg) - }, fs.persist) + return fs.persist(newCfg) } // persist writes the configuration to the configured file. func (fs *FileStore) persist(cfg *model.Config) error { - fs.stopWatcher() + needsRestart := false + if fs.watcher != nil { + fs.stopWatcher() + needsRestart = true + } b, err := marshalConfig(cfg) if err != nil { @@ -128,8 +119,8 @@ func (fs *FileStore) persist(cfg *model.Config) error { return errors.Wrap(err, "failed to write file") } - if fs.watch { - if err = fs.startWatcher(); err != nil { + if fs.watch && needsRestart { + if err = fs.Watch(fs.callback); err != nil { mlog.Error("failed to start config watcher", mlog.String("path", fs.path), mlog.Err(err)) } } @@ -138,35 +129,22 @@ func (fs *FileStore) persist(cfg *model.Config) error { } // Load updates the current configuration from the backing store. -func (fs *FileStore) Load() (err error) { - var needsSave bool - var f io.ReadCloser - - f, err = os.Open(fs.path) +func (fs *FileStore) Load() ([]byte, error) { + f, err := os.Open(fs.path) if os.IsNotExist(err) { - needsSave = true - defaultCfg := &model.Config{} - defaultCfg.SetDefaults() - - var defaultCfgBytes []byte - defaultCfgBytes, err = marshalConfig(defaultCfg) - if err != nil { - return errors.Wrap(err, "failed to serialize default config") - } - - f = ioutil.NopCloser(bytes.NewReader(defaultCfgBytes)) + return nil, nil } else if err != nil { - return errors.Wrapf(err, "failed to open %s for reading", fs.path) + return nil, errors.Wrapf(err, "failed to open %s for reading", fs.path) } - defer func() { - closeErr := f.Close() - if err == nil && closeErr != nil { - err = errors.Wrap(closeErr, "failed to close") - } - }() + defer f.Close() - return fs.commonStore.load(f, needsSave, fs.commonStore.validate, fs.persist) + fileBytes, err := ioutil.ReadAll(f) + if err != nil { + return nil, err + } + + return fileBytes, nil } // GetFile fetches the contents of a previously persisted configuration file. @@ -237,17 +215,13 @@ func (fs *FileStore) RemoveFile(name string) error { return nil } -// startWatcher starts a watcher to monitor for external config file changes. -func (fs *FileStore) startWatcher() error { - if fs.watcher != nil { +func (fs *FileStore) Watch(callback func()) error { + if fs.watcher != nil || !fs.watch { return nil } - watcher, err := newWatcher(fs.path, func() { - if err := fs.Load(); err != nil { - mlog.Error("failed to reload file on change", mlog.String("path", fs.path), mlog.Err(err)) - } - }) + fs.callback = callback + watcher, err := newWatcher(fs.path, callback) if err != nil { return err } @@ -276,9 +250,6 @@ func (fs *FileStore) String() string { // Close cleans up resources associated with the store. func (fs *FileStore) Close() error { - fs.configLock.Lock() - defer fs.configLock.Unlock() - fs.stopWatcher() return nil diff --git a/config/file_test.go b/config/file_test.go index b775c7fea0..ef17a43d74 100644 --- a/config/file_test.go +++ b/config/file_test.go @@ -4,6 +4,7 @@ package config_test import ( + "encoding/json" "io/ioutil" "os" "path/filepath" @@ -56,7 +57,8 @@ func getActualFileConfig(t *testing.T, path string) *model.Config { require.Nil(t, err) defer f.Close() - actualCfg, _, err := config.UnmarshalConfig(f, false) + var actualCfg *model.Config + err = json.NewDecoder(f).Decode(&actualCfg) require.Nil(t, err) return actualCfg @@ -89,9 +91,11 @@ func TestFileStoreNew(t *testing.T) { fs, err := config.NewFileStore(path, false) require.NoError(t, err) - defer fs.Close() + configStore, err := config.NewStoreFromBacking(fs) + require.NoError(t, err) + defer configStore.Close() - assert.Equal(t, "http://TestStoreNew", *fs.Get().ServiceSettings.SiteURL) + assert.Equal(t, "http://TestStoreNew", *configStore.Get().ServiceSettings.SiteURL) assertFileNotEqualsConfig(t, testConfig, path) }) @@ -101,9 +105,11 @@ func TestFileStoreNew(t *testing.T) { fs, err := config.NewFileStore(path, false) require.NoError(t, err) - defer fs.Close() + configStore, err := config.NewStoreFromBacking(fs) + require.NoError(t, err) + defer configStore.Close() - assert.Equal(t, "http://minimal", *fs.Get().ServiceSettings.SiteURL) + assert.Equal(t, "http://minimal", *configStore.Get().ServiceSettings.SiteURL) assertFileEqualsConfig(t, minimalConfig, path) }) @@ -118,9 +124,11 @@ func TestFileStoreNew(t *testing.T) { path := filepath.Join(tempDir, "does_not_exist") fs, err := config.NewFileStore(path, false) require.NoError(t, err) - defer fs.Close() + configStore, err := config.NewStoreFromBacking(fs) + require.NoError(t, err) + defer configStore.Close() - assert.Equal(t, "", *fs.Get().ServiceSettings.SiteURL) + assert.Equal(t, "", *configStore.Get().ServiceSettings.SiteURL) assertFileNotEqualsConfig(t, testConfig, path) }) @@ -133,7 +141,10 @@ func TestFileStoreNew(t *testing.T) { defer os.RemoveAll(tempDir) path := filepath.Join(tempDir, "does/not/exist") - _, err = config.NewFileStore(path, false) + fs, err := config.NewFileStore(path, false) + require.NoError(t, err) + configStore, err := config.NewStoreFromBacking(fs) + require.Nil(t, configStore) require.Error(t, err) }) @@ -154,9 +165,11 @@ func TestFileStoreNew(t *testing.T) { fs, err := config.NewFileStore(path, false) require.NoError(t, err) - defer fs.Close() + configStore, err := config.NewStoreFromBacking(fs) + require.NoError(t, err) + defer configStore.Close() - assert.Equal(t, "http://TestStoreNew", *fs.Get().ServiceSettings.SiteURL) + assert.Equal(t, "http://TestStoreNew", *configStore.Get().ServiceSettings.SiteURL) assertFileNotEqualsConfig(t, testConfig, path) }) @@ -171,9 +184,11 @@ func TestFileStoreNew(t *testing.T) { path := "TestFileStoreNew/a/b/c/config.json" fs, err := config.NewFileStore(path, false) require.NoError(t, err) - defer fs.Close() + configStore, err := config.NewStoreFromBacking(fs) + require.NoError(t, err) + defer configStore.Close() - assert.Equal(t, "", *fs.Get().ServiceSettings.SiteURL) + assert.Equal(t, "", *configStore.Get().ServiceSettings.SiteURL) assertFileNotEqualsConfig(t, testConfig, filepath.Join("config", path)) }) } @@ -184,21 +199,22 @@ func TestFileStoreGet(t *testing.T) { fs, err := config.NewFileStore(path, false) require.NoError(t, err) - defer fs.Close() + configStore, err := config.NewStoreFromBacking(fs) + require.NoError(t, err) + defer configStore.Close() - cfg := fs.Get() + cfg := configStore.Get() assert.Equal(t, "http://TestStoreNew", *cfg.ServiceSettings.SiteURL) - cfg2 := fs.Get() + cfg2 := configStore.Get() assert.Equal(t, "http://TestStoreNew", *cfg.ServiceSettings.SiteURL) assert.True(t, cfg == cfg2, "Get() returned different configuration instances") newCfg := &model.Config{} - oldCfg, err := fs.Set(newCfg) + _, err = configStore.Set(newCfg) require.NoError(t, err) - assert.True(t, oldCfg == cfg, "returned config after set() changed original") assert.False(t, newCfg == cfg, "returned config should have been different from original") } @@ -207,7 +223,9 @@ func TestFileStoreGetEnivironmentOverrides(t *testing.T) { path, tearDown := setupConfigFile(t, testConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -217,7 +235,9 @@ func TestFileStoreGetEnivironmentOverrides(t *testing.T) { os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override") defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") - fs, err = config.NewFileStore(path, false) + fsInner, err = config.NewFileStore(path, false) + require.NoError(t, err) + fs, err = config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -229,7 +249,9 @@ func TestFileStoreGetEnivironmentOverrides(t *testing.T) { path, tearDown := setupConfigFile(t, testConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -239,7 +261,9 @@ func TestFileStoreGetEnivironmentOverrides(t *testing.T) { os.Setenv("MM_PLUGINSETTINGS_ENABLEUPLOADS", "true") defer os.Unsetenv("MM_PLUGINSETTINGS_ENABLEUPLOADS") - fs, err = config.NewFileStore(path, false) + fsInner, err = config.NewFileStore(path, false) + require.NoError(t, err) + fs, err = config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -251,7 +275,9 @@ func TestFileStoreGetEnivironmentOverrides(t *testing.T) { path, tearDown := setupConfigFile(t, testConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -261,7 +287,9 @@ func TestFileStoreGetEnivironmentOverrides(t *testing.T) { os.Setenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM", "3000") defer os.Unsetenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM") - fs, err = config.NewFileStore(path, false) + fsInner, err = config.NewFileStore(path, false) + require.NoError(t, err) + fs, err = config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -273,7 +301,9 @@ func TestFileStoreGetEnivironmentOverrides(t *testing.T) { path, tearDown := setupConfigFile(t, testConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -283,7 +313,9 @@ func TestFileStoreGetEnivironmentOverrides(t *testing.T) { os.Setenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE", "123456") defer os.Unsetenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE") - fs, err = config.NewFileStore(path, false) + fsInner, err = config.NewFileStore(path, false) + require.NoError(t, err) + fs, err = config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -295,7 +327,9 @@ func TestFileStoreGetEnivironmentOverrides(t *testing.T) { path, tearDown := setupConfigFile(t, testConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -305,7 +339,9 @@ func TestFileStoreGetEnivironmentOverrides(t *testing.T) { os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db") defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS") - fs, err = config.NewFileStore(path, false) + fsInner, err = config.NewFileStore(path, false) + require.NoError(t, err) + fs, err = config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -320,7 +356,9 @@ func TestFileStoreGetEnivironmentOverrides(t *testing.T) { path, tearDown := setupConfigFile(t, testConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -330,7 +368,9 @@ func TestFileStoreGetEnivironmentOverrides(t *testing.T) { os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db user:pwd@db2:5433/test-db2 user:pwd@db3:5434/test-db3") defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS") - fs, err = config.NewFileStore(path, false) + fsInner, err = config.NewFileStore(path, false) + require.NoError(t, err) + fs, err = config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -340,37 +380,23 @@ func TestFileStoreGetEnivironmentOverrides(t *testing.T) { } func TestFileStoreSet(t *testing.T) { - t.Run("set same pointer value", func(t *testing.T) { - t.Skip("not yet implemented") - - path, tearDown := setupConfigFile(t, emptyConfig) - defer tearDown() - - fs, err := config.NewFileStore(path, false) - require.NoError(t, err) - defer fs.Close() - - _, err = fs.Set(fs.Get()) - if assert.Error(t, err) { - assert.EqualError(t, err, "old configuration modified instead of cloning") - } - }) - t.Run("defaults required", func(t *testing.T) { path, tearDown := setupConfigFile(t, minimalConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() - oldCfg := fs.Get() + oldCfg := fs.Get().Clone() newCfg := &model.Config{} retCfg, err := fs.Set(newCfg) require.NoError(t, err) - assert.Equal(t, oldCfg, retCfg) + require.Equal(t, oldCfg, retCfg) assert.Equal(t, "", *fs.Get().ServiceSettings.SiteURL) }) @@ -379,18 +405,17 @@ func TestFileStoreSet(t *testing.T) { path, tearDown := setupConfigFile(t, ldapConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() - oldCfg := fs.Get() - newCfg := &model.Config{} newCfg.LdapSettings.BindPassword = sToP(model.FAKE_SETTING) - retCfg, err := fs.Set(newCfg) + _, err = fs.Set(newCfg) require.NoError(t, err) - assert.Equal(t, oldCfg, retCfg) assert.Equal(t, "password", *fs.Get().LdapSettings.BindPassword) }) @@ -399,7 +424,9 @@ func TestFileStoreSet(t *testing.T) { path, tearDown := setupConfigFile(t, emptyConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -418,13 +445,13 @@ func TestFileStoreSet(t *testing.T) { path, tearDown := setupConfigFile(t, readOnlyConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() - newCfg := &model.Config{} - - _, err = fs.Set(newCfg) + _, err = fs.Set(readOnlyConfig) if assert.Error(t, err) { assert.Equal(t, config.ErrReadOnlyConfiguration, errors.Cause(err)) } @@ -437,7 +464,9 @@ func TestFileStoreSet(t *testing.T) { path, tearDown := setupConfigFile(t, emptyConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -457,12 +486,12 @@ func TestFileStoreSet(t *testing.T) { path, tearDown := setupConfigFile(t, emptyConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() - oldCfg := fs.Get() - called := make(chan bool, 1) callback := func(oldfg, newCfg *model.Config) { called <- true @@ -471,9 +500,8 @@ func TestFileStoreSet(t *testing.T) { newCfg := &model.Config{} - retCfg, err := fs.Set(newCfg) + _, err = fs.Set(newCfg) require.NoError(t, err) - assert.Equal(t, oldCfg, retCfg) require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config written") }) @@ -486,7 +514,9 @@ func TestFileStoreSet(t *testing.T) { path, tearDown := setupConfigFile(t, emptyConfig) defer tearDown() - fs, err := config.NewFileStore(path, true) + fsInner, err := config.NewFileStore(path, true) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -516,7 +546,9 @@ func TestFileStoreLoad(t *testing.T) { path, tearDown := setupConfigFile(t, emptyConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -531,7 +563,9 @@ func TestFileStoreLoad(t *testing.T) { path, tearDown := setupConfigFile(t, minimalConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -553,7 +587,9 @@ func TestFileStoreLoad(t *testing.T) { os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://overridePersistEnvVariables") defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -576,7 +612,9 @@ func TestFileStoreLoad(t *testing.T) { os.Setenv("MM_PLUGINSETTINGS_ENABLEUPLOADS", "true") defer os.Unsetenv("MM_PLUGINSETTINGS_ENABLEUPLOADS") - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -599,7 +637,9 @@ func TestFileStoreLoad(t *testing.T) { os.Setenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM", "3000") defer os.Unsetenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM") - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -622,7 +662,9 @@ func TestFileStoreLoad(t *testing.T) { os.Setenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE", "123456") defer os.Unsetenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE") - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -645,7 +687,9 @@ func TestFileStoreLoad(t *testing.T) { os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db") defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS") - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -670,7 +714,9 @@ func TestFileStoreLoad(t *testing.T) { os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db") defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS") - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -690,7 +736,9 @@ func TestFileStoreLoad(t *testing.T) { path, tearDown := setupConfigFile(t, emptyConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -709,7 +757,9 @@ func TestFileStoreLoad(t *testing.T) { path, tearDown := setupConfigFile(t, fixesRequiredConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -726,7 +776,9 @@ func TestFileStoreLoad(t *testing.T) { path, tearDown := setupConfigFile(t, emptyConfig) defer tearDown() - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -754,7 +806,9 @@ func TestFileStoreWatcherEmitter(t *testing.T) { defer tearDown() t.Run("disabled", func(t *testing.T) { - fs, err := config.NewFileStore(path, false) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -776,7 +830,9 @@ func TestFileStoreWatcherEmitter(t *testing.T) { }) t.Run("enabled", func(t *testing.T) { - fs, err := config.NewFileStore(path, true) + fsInner, err := config.NewFileStore(path, true) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() @@ -799,7 +855,9 @@ func TestFileStoreSave(t *testing.T) { path, tearDown := setupConfigFile(t, minimalConfig) defer tearDown() - fs, err := config.NewFileStore(path, true) + fsInner, err := config.NewFileStore(path, false) + require.NoError(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.NoError(t, err) defer fs.Close() diff --git a/config/memory.go b/config/memory.go index 639a933bdf..c1a99a81ea 100644 --- a/config/memory.go +++ b/config/memory.go @@ -4,9 +4,7 @@ package config import ( - "bytes" "fmt" - "io/ioutil" "github.com/pkg/errors" @@ -14,9 +12,8 @@ import ( ) // MemoryStore implements the Store interface. It is meant primarily for testing. +// Not to be used directly. Only to be used as a backing store for config.Store type MemoryStore struct { - commonStore - allowEnvironmentOverrides bool validate bool files map[string][]byte @@ -56,24 +53,12 @@ func NewMemoryStoreWithOptions(options *MemoryStoreOptions) (*MemoryStore, error savedConfig: savedConfig, } - ms.commonStore.config = &model.Config{} - ms.commonStore.config.SetDefaults() - - if err := ms.Load(); err != nil { - return nil, err - } - return ms, nil } // Set replaces the current configuration in its entirety. -func (ms *MemoryStore) Set(newCfg *model.Config) (*model.Config, error) { - validate := ms.commonStore.validate - if !ms.validate { - validate = nil - } - - return ms.commonStore.set(newCfg, ms.allowEnvironmentOverrides, validate, ms.persist) +func (ms *MemoryStore) Set(newCfg *model.Config) error { + return ms.persist(newCfg) } // persist copies the active config to the saved config. @@ -84,28 +69,18 @@ func (ms *MemoryStore) persist(cfg *model.Config) error { } // Load applies environment overrides to the default config as if a re-load had occurred. -func (ms *MemoryStore) Load() (err error) { - var cfgBytes []byte - cfgBytes, err = marshalConfig(ms.savedConfig) +func (ms *MemoryStore) Load() ([]byte, error) { + cfgBytes, err := marshalConfig(ms.savedConfig) if err != nil { - return errors.Wrap(err, "failed to serialize config") + return nil, errors.Wrap(err, "failed to serialize config") } - f := ioutil.NopCloser(bytes.NewReader(cfgBytes)) + return cfgBytes, nil - validate := ms.commonStore.validate - if !ms.validate { - validate = nil - } - - return ms.commonStore.load(f, false, validate, ms.persist) } // GetFile fetches the contents of a previously persisted configuration file. func (ms *MemoryStore) GetFile(name string) ([]byte, error) { - ms.configLock.RLock() - defer ms.configLock.RUnlock() - data, ok := ms.files[name] if !ok { return nil, fmt.Errorf("file %s not stored", name) @@ -116,9 +91,6 @@ func (ms *MemoryStore) GetFile(name string) ([]byte, error) { // SetFile sets or replaces the contents of a configuration file. func (ms *MemoryStore) SetFile(name string, data []byte) error { - ms.configLock.Lock() - defer ms.configLock.Unlock() - ms.files[name] = data return nil @@ -126,18 +98,12 @@ func (ms *MemoryStore) SetFile(name string, data []byte) error { // HasFile returns true if the given file was previously persisted. func (ms *MemoryStore) HasFile(name string) (bool, error) { - ms.configLock.RLock() - defer ms.configLock.RUnlock() - _, ok := ms.files[name] return ok, nil } // RemoveFile removes a previously persisted configuration file. func (ms *MemoryStore) RemoveFile(name string) error { - ms.configLock.Lock() - defer ms.configLock.Unlock() - delete(ms.files, name) return nil @@ -152,3 +118,8 @@ func (ms *MemoryStore) String() string { func (ms *MemoryStore) Close() error { return nil } + +// Watch nothing on memory store +func (ms *MemoryStore) Watch(_ func()) error { + return nil +} diff --git a/config/memory_test.go b/config/memory_test.go index d1c8836407..38f7565ffc 100644 --- a/config/memory_test.go +++ b/config/memory_test.go @@ -6,13 +6,11 @@ package config_test import ( "os" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/v5/config" - "github.com/mattermost/mattermost-server/v5/model" ) func setupConfigMemory(t *testing.T) { @@ -20,254 +18,6 @@ func setupConfigMemory(t *testing.T) { os.Clearenv() } -func TestMemoryStoreNew(t *testing.T) { - t.Run("no existing configuration - initialization required", func(t *testing.T) { - ms, err := config.NewMemoryStore() - require.NoError(t, err) - defer ms.Close() - - assert.Equal(t, "", *ms.Get().ServiceSettings.SiteURL) - }) - - t.Run("existing config, initialization required", func(t *testing.T) { - setupConfigMemory(t) - - ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: testConfig}) - require.NoError(t, err) - defer ms.Close() - - assert.Equal(t, "http://TestStoreNew", *ms.Get().ServiceSettings.SiteURL) - }) - - t.Run("already minimally configured", func(t *testing.T) { - setupConfigMemory(t) - - ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: minimalConfig}) - require.NoError(t, err) - defer ms.Close() - - assert.Equal(t, "http://minimal", *ms.Get().ServiceSettings.SiteURL) - }) - - t.Run("invalid config, validation enabled", func(t *testing.T) { - _, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: invalidConfig}) - require.Error(t, err) - }) - - t.Run("invalid config, validation disabled", func(t *testing.T) { - _, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: invalidConfig, SkipValidation: true}) - require.NoError(t, err) - }) -} - -func TestMemoryStoreGet(t *testing.T) { - setupConfigMemory(t) - - ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: testConfig}) - require.NoError(t, err) - defer ms.Close() - - cfg := ms.Get() - assert.Equal(t, "http://TestStoreNew", *cfg.ServiceSettings.SiteURL) - - cfg2 := ms.Get() - assert.Equal(t, "http://TestStoreNew", *cfg.ServiceSettings.SiteURL) - - assert.True(t, cfg == cfg2, "Get() returned different configuration instances") - - newCfg := &model.Config{} - oldCfg, err := ms.Set(newCfg) - require.NoError(t, err) - - assert.True(t, oldCfg == cfg, "returned config after set() changed original") - assert.False(t, newCfg == cfg, "returned config should have been different from original") -} - -func TestMemoryStoreGetEnivironmentOverrides(t *testing.T) { - setupConfigMemory(t) - - ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: testConfig}) - require.NoError(t, err) - defer ms.Close() - - assert.Equal(t, "http://TestStoreNew", *ms.Get().ServiceSettings.SiteURL) - assert.Empty(t, ms.GetEnvironmentOverrides()) - - os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override") - defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") - - ms, err = config.NewMemoryStore() - require.NoError(t, err) - defer ms.Close() - - assert.Equal(t, "http://override", *ms.Get().ServiceSettings.SiteURL) - assert.Equal(t, map[string]interface{}{"ServiceSettings": map[string]interface{}{"SiteURL": true}}, ms.GetEnvironmentOverrides()) -} - -func TestMemoryStoreSet(t *testing.T) { - t.Run("set same pointer value", func(t *testing.T) { - t.Skip("not yet implemented") - - setupConfigMemory(t) - - ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: emptyConfig}) - require.NoError(t, err) - defer ms.Close() - - _, err = ms.Set(ms.Get()) - if assert.Error(t, err) { - assert.EqualError(t, err, "old configuration modified instead of cloning") - } - }) - - t.Run("defaults required", func(t *testing.T) { - setupConfigMemory(t) - - ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: minimalConfig}) - require.NoError(t, err) - defer ms.Close() - - oldCfg := ms.Get() - - newCfg := &model.Config{} - - retCfg, err := ms.Set(newCfg) - require.NoError(t, err) - assert.Equal(t, oldCfg, retCfg) - - assert.Equal(t, "", *ms.Get().ServiceSettings.SiteURL) - }) - - t.Run("desanitization required", func(t *testing.T) { - setupConfigMemory(t) - - ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: ldapConfig}) - require.NoError(t, err) - defer ms.Close() - - oldCfg := ms.Get() - - newCfg := &model.Config{} - newCfg.LdapSettings.BindPassword = sToP(model.FAKE_SETTING) - - retCfg, err := ms.Set(newCfg) - require.NoError(t, err) - assert.Equal(t, oldCfg, retCfg) - - assert.Equal(t, "password", *ms.Get().LdapSettings.BindPassword) - }) - - t.Run("invalid", func(t *testing.T) { - setupConfigMemory(t) - - ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: emptyConfig}) - require.NoError(t, err) - defer ms.Close() - - newCfg := &model.Config{} - newCfg.ServiceSettings.SiteURL = sToP("invalid") - - _, err = ms.Set(newCfg) - if assert.Error(t, err) { - assert.EqualError(t, err, "new configuration is invalid: Config.IsValid: model.config.is_valid.site_url.app_error, ") - } - - assert.Equal(t, "", *ms.Get().ServiceSettings.SiteURL) - }) - - t.Run("read-only ignored", func(t *testing.T) { - setupConfigMemory(t) - - ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: readOnlyConfig}) - require.NoError(t, err) - defer ms.Close() - - newCfg := &model.Config{ - ServiceSettings: model.ServiceSettings{ - SiteURL: sToP("http://new"), - }, - } - - _, err = ms.Set(newCfg) - require.NoError(t, err) - - assert.Equal(t, "http://new", *ms.Get().ServiceSettings.SiteURL) - }) - - t.Run("listeners notified", func(t *testing.T) { - setupConfigMemory(t) - - ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: emptyConfig}) - require.NoError(t, err) - defer ms.Close() - - oldCfg := ms.Get() - - called := make(chan bool, 1) - callback := func(oldfg, newCfg *model.Config) { - called <- true - } - ms.AddListener(callback) - - newCfg := &model.Config{} - - retCfg, err := ms.Set(newCfg) - require.NoError(t, err) - assert.Equal(t, oldCfg, retCfg) - - require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config written") - }) -} - -func TestMemoryStoreLoad(t *testing.T) { - t.Run("honour environment", func(t *testing.T) { - setupConfigMemory(t) - - ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: minimalConfig}) - require.NoError(t, err) - defer ms.Close() - - os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override") - defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") - - err = ms.Load() - require.NoError(t, err) - assert.Equal(t, "http://override", *ms.Get().ServiceSettings.SiteURL) - assert.Equal(t, map[string]interface{}{"ServiceSettings": map[string]interface{}{"SiteURL": true}}, ms.GetEnvironmentOverrides()) - }) - - t.Run("fixes required", func(t *testing.T) { - setupConfigMemory(t) - - ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: fixesRequiredConfig}) - require.NoError(t, err) - defer ms.Close() - - err = ms.Load() - require.NoError(t, err) - assert.Equal(t, "http://trailingslash", *ms.Get().ServiceSettings.SiteURL) - }) - - t.Run("listeners notifed", func(t *testing.T) { - setupConfigMemory(t) - - ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: emptyConfig}) - require.NoError(t, err) - defer ms.Close() - - called := make(chan bool, 1) - callback := func(oldfg, newCfg *model.Config) { - called <- true - } - ms.AddListener(callback) - - err = ms.Load() - require.NoError(t, err) - - require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config loaded") - }) -} - func TestMemoryGetFile(t *testing.T) { setupConfigMemory(t) diff --git a/config/migrate.go b/config/migrate.go index 7070591c2d..4ab917c479 100644 --- a/config/migrate.go +++ b/config/migrate.go @@ -46,7 +46,7 @@ func Migrate(from, to string) error { return nil } -func migrateFile(name string, source Store, destination Store) error { +func migrateFile(name string, source *Store, destination *Store) error { fileExists, err := source.HasFile(name) if err != nil { return errors.Wrapf(err, "failed to check existence of %s", name) diff --git a/config/migrate_test.go b/config/migrate_test.go index 033959e808..ffa13d658d 100644 --- a/config/migrate_test.go +++ b/config/migrate_test.go @@ -46,7 +46,7 @@ func TestMigrate(t *testing.T) { truncateTables(t) } - setupSource := func(t *testing.T, source config.Store) { + setupSource := func(t *testing.T, source *config.Store) { t.Helper() cfg := source.Get() @@ -74,7 +74,7 @@ func TestMigrate(t *testing.T) { } } - assertDestination := func(t *testing.T, destination config.Store, source config.Store) { + assertDestination := func(t *testing.T, destination *config.Store, source *config.Store) { t.Helper() for i, file := range files { @@ -100,7 +100,9 @@ func TestMigrate(t *testing.T) { destinationDSN := path.Join(pwd, "config-custom.json") sourceDSN := getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource) - source, err := config.NewDatabaseStore(sourceDSN) + sourcedb, err := config.NewDatabaseStore(sourceDSN) + require.NoError(t, err) + source, err := config.NewStoreFromBacking(sourcedb) require.NoError(t, err) defer source.Close() @@ -108,7 +110,9 @@ func TestMigrate(t *testing.T) { err = config.Migrate(sourceDSN, destinationDSN) require.NoError(t, err) - destination, err := config.NewFileStore(destinationDSN, false) + destinationfile, err := config.NewFileStore(destinationDSN, false) + require.NoError(t, err) + destination, err := config.NewStoreFromBacking(destinationfile) require.NoError(t, err) defer destination.Close() @@ -125,7 +129,9 @@ func TestMigrate(t *testing.T) { sourceDSN := path.Join(pwd, "config-custom.json") destinationDSN := getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource) - source, err := config.NewFileStore(sourceDSN, false) + sourcefile, err := config.NewFileStore(sourceDSN, false) + require.NoError(t, err) + source, err := config.NewStoreFromBacking(sourcefile) require.NoError(t, err) defer source.Close() @@ -133,7 +139,9 @@ func TestMigrate(t *testing.T) { err = config.Migrate(sourceDSN, destinationDSN) require.NoError(t, err) - destination, err := config.NewDatabaseStore(destinationDSN) + destinationdb, err := config.NewDatabaseStore(destinationDSN) + require.NoError(t, err) + destination, err := config.NewStoreFromBacking(destinationdb) require.NoError(t, err) defer destination.Close() diff --git a/config/split_logger.go b/config/split_logger.go new file mode 100644 index 0000000000..10f9d81de5 --- /dev/null +++ b/config/split_logger.go @@ -0,0 +1,35 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package config + +import ( + "fmt" + + "github.com/mattermost/mattermost-server/v5/mlog" +) + +type splitLogger struct { + wrappedLog *mlog.Logger +} + +func (s *splitLogger) Error(msg ...interface{}) { + s.wrappedLog.Error(fmt.Sprint(msg...)) +} + +func (s *splitLogger) Warning(msg ...interface{}) { + s.wrappedLog.Warn(fmt.Sprint(msg...)) +} + +// Ignoring more verbose messages from split +func (s *splitLogger) Info(msg ...interface{}) { + //s.wrappedLog.Info(fmt.Sprint(msg...)) +} + +func (s *splitLogger) Debug(msg ...interface{}) { + //s.wrappedLog.Debug(fmt.Sprint(msg...)) +} + +func (s *splitLogger) Verbose(msg ...interface{}) { + //s.wrappedLog.Info(fmt.Sprint(msg...)) +} diff --git a/config/store.go b/config/store.go index 6be194a419..e0701f67da 100644 --- a/config/store.go +++ b/config/store.go @@ -4,37 +4,26 @@ package config import ( + "bytes" + "encoding/json" "strings" + "sync" "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/utils/jsonutils" + "github.com/pkg/errors" ) // Listener is a callback function invoked when the configuration changes. type Listener func(oldConfig *model.Config, newConfig *model.Config) -// Store abstracts the act of getting and setting the configuration. -type Store interface { - // Get fetches the current, cached configuration. - Get() *model.Config - - // GetEnvironmentOverrides fetches the configuration fields overridden by environment variables. - GetEnvironmentOverrides() map[string]interface{} - - // RemoveEnvironmentOverrides returns a new config without the environment - // overrides - RemoveEnvironmentOverrides(cfg *model.Config) *model.Config - +type BackingStore interface { // Set replaces the current configuration in its entirety and updates the backing store. - Set(*model.Config) (*model.Config, error) + Set(*model.Config) error - // Load updates the current configuration from the backing store, possibly initializing. - Load() (err error) - - // AddListener adds a callback function to invoke when the configuration is modified. - AddListener(listener Listener) string - - // RemoveListener removes a callback function using an id returned from AddListener. - RemoveListener(id string) + // Load retrieves the configuration stored. If there is no configuration stored + // the io.ReadCloser will be nil + Load() ([]byte, error) // GetFile fetches the contents of a previously persisted configuration file. // If no such file exists, an empty byte array will be returned without error. @@ -52,15 +41,247 @@ type Store interface { // String describes the backing store for the config. String() string + Watch(callback func()) error + // Close cleans up resources associated with the store. Close() error } // NewStore creates a database or file store given a data source name by which to connect. -func NewStore(dsn string, watch bool) (Store, error) { +func NewStore(dsn string, watch bool) (*Store, error) { + backingStore, err := getBackingStore(dsn, watch) + if err != nil { + return nil, err + } + + return NewStoreFromBacking(backingStore) + +} + +func NewStoreFromBacking(backingStore BackingStore) (*Store, error) { + store := &Store{ + backingStore: backingStore, + } + + if err := store.Load(); err != nil { + return nil, errors.Wrap(err, "unable to load on store creation") + } + + if err := backingStore.Watch(func() { + store.Load() + }); err != nil { + return nil, errors.Wrap(err, "failed to watch backing store") + } + + return store, nil +} + +func getBackingStore(dsn string, watch bool) (BackingStore, error) { if strings.HasPrefix(dsn, "mysql://") || strings.HasPrefix(dsn, "postgres://") { return NewDatabaseStore(dsn) } return NewFileStore(dsn, watch) } + +func NewTestMemoryStore() *Store { + memoryStore, err := NewMemoryStore() + if err != nil { + panic("failed to initialize memory store: " + err.Error()) + } + + configStore, err := NewStoreFromBacking(memoryStore) + if err != nil { + panic("failed to initialize config store: " + err.Error()) + } + + return configStore +} + +type Store struct { + emitter + backingStore BackingStore + + configLock sync.RWMutex + config *model.Config + configNoEnv *model.Config + + persistFeatureFlags bool +} + +// Get fetches the current, cached configuration. +func (s *Store) Get() *model.Config { + s.configLock.RLock() + defer s.configLock.RUnlock() + return s.config +} + +// Get fetches the current, cached configuration without environment variable overrides. +func (s *Store) GetNoEnv() *model.Config { + s.configLock.RLock() + defer s.configLock.RUnlock() + return s.configNoEnv +} + +// GetEnvironmentOverrides fetches the configuration fields overridden by environment variables. +func (s *Store) GetEnvironmentOverrides() map[string]interface{} { + return generateEnvironmentMap(GetEnvironment()) +} + +// RemoveEnvironmentOverrides returns a new config without the environment +// overrides +func (s *Store) RemoveEnvironmentOverrides(cfg *model.Config) *model.Config { + s.configLock.RLock() + defer s.configLock.RUnlock() + return removeEnvOverrides(cfg, s.configNoEnv, s.GetEnvironmentOverrides()) +} + +// PersistFeatures sets if the store should persist feature flags. +func (s *Store) PersistFeatures(persist bool) { + s.configLock.Lock() + defer s.configLock.Unlock() + s.persistFeatureFlags = persist +} + +// Set replaces the current configuration in its entirety and updates the backing store. +func (s *Store) Set(newCfg *model.Config) (*model.Config, error) { + s.configLock.Lock() + var unlockOnce sync.Once + defer unlockOnce.Do(s.configLock.Unlock) + + oldCfg := s.config.Clone() + + // Really just for some tests we need to set defaults here + newCfg.SetDefaults() + + // Sometimes the config is received with "fake" data in sensitive fields. Apply the real + // data from the existing config as necessary. + desanitize(oldCfg, newCfg) + + if err := newCfg.IsValid(); err != nil { + return nil, errors.Wrap(err, "new configuration is invalid") + } + + newCfg = removeEnvOverrides(newCfg, s.configNoEnv, s.GetEnvironmentOverrides()) + + // Don't persist feature flags unless we are on MM cloud + // MM cloud uses config in the DB as a cache of the feature flag + // settings in case the managment system is down when a pod starts. + if !s.persistFeatureFlags { + newCfg.FeatureFlags = nil + } + + if err := s.backingStore.Set(newCfg); err != nil { + return nil, errors.Wrap(err, "failed to persist") + } + + if err := s.loadLockedWithOld(oldCfg, &unlockOnce); err != nil { + return nil, errors.Wrap(err, "failed to load on save") + } + + return oldCfg, nil +} + +func (s *Store) loadLockedWithOld(oldCfg *model.Config, unlockOnce *sync.Once) error { + configBytes, err := s.backingStore.Load() + if err != nil { + return err + } + + loadedConfig := &model.Config{} + if len(configBytes) != 0 { + if err = json.Unmarshal(configBytes, &loadedConfig); err != nil { + return jsonutils.HumanizeJsonError(err, configBytes) + } + } + + loadedConfig.SetDefaults() + + s.configNoEnv = loadedConfig.Clone() + fixConfig(s.configNoEnv) + + loadedConfig = applyEnvironmentMap(loadedConfig, GetEnvironment()) + + fixConfig(loadedConfig) + + if err := loadedConfig.IsValid(); err != nil { + return errors.Wrap(err, "invalid config") + } + + // Apply changes that may have happened on load to the backing store. + oldCfgBytes, err := json.Marshal(oldCfg) + if err != nil { + return errors.Wrap(err, "failed to marshal old config") + } + newCfgBytes, err := json.Marshal(loadedConfig) + if err != nil { + return errors.Wrap(err, "failed to marshal loaded config") + } + if len(configBytes) == 0 || !bytes.Equal(oldCfgBytes, newCfgBytes) { + if err := s.backingStore.Set(s.configNoEnv); err != nil { + if !errors.Is(err, ErrReadOnlyConfiguration) { + return errors.Wrap(err, "failed to persist") + } + } + } + + s.config = loadedConfig + + unlockOnce.Do(s.configLock.Unlock) + + s.invokeConfigListeners(oldCfg, loadedConfig) + + return nil +} + +// Load updates the current configuration from the backing store, possibly initializing. +func (s *Store) Load() error { + s.configLock.Lock() + var unlockOnce sync.Once + defer unlockOnce.Do(s.configLock.Unlock) + + oldCfg := s.config.Clone() + + return s.loadLockedWithOld(oldCfg, &unlockOnce) +} + +// GetFile fetches the contents of a previously persisted configuration file. +// If no such file exists, an empty byte array will be returned without error. +func (s *Store) GetFile(name string) ([]byte, error) { + s.configLock.RLock() + defer s.configLock.RUnlock() + return s.backingStore.GetFile(name) +} + +// SetFile sets or replaces the contents of a configuration file. +func (s *Store) SetFile(name string, data []byte) error { + s.configLock.Lock() + defer s.configLock.Unlock() + return s.backingStore.SetFile(name, data) +} + +// HasFile returns true if the given file was previously persisted. +func (s *Store) HasFile(name string) (bool, error) { + s.configLock.RLock() + defer s.configLock.RUnlock() + return s.backingStore.HasFile(name) +} + +// RemoveFile removes a previously persisted configuration file. +func (s *Store) RemoveFile(name string) error { + s.configLock.Lock() + defer s.configLock.Unlock() + return s.backingStore.RemoveFile(name) +} + +// String describes the backing store for the config. +func (s *Store) String() string { + return s.backingStore.String() +} + +// Close cleans up resources associated with the store. +func (s *Store) Close() error { + s.configLock.Lock() + defer s.configLock.Unlock() + return s.backingStore.Close() +} diff --git a/config/unmarshal.go b/config/unmarshal.go index a103fb255d..e751e9ac57 100644 --- a/config/unmarshal.go +++ b/config/unmarshal.go @@ -4,129 +4,12 @@ package config import ( - "bytes" "encoding/json" - "io" - "io/ioutil" - "reflect" - "strings" - "github.com/mattermost/viper" - "github.com/pkg/errors" - - "github.com/mattermost/mattermost-server/v5/mlog" "github.com/mattermost/mattermost-server/v5/model" - "github.com/mattermost/mattermost-server/v5/utils/jsonutils" ) -// newViper creates an instance of viper.Viper configured for parsing a configuration. -func newViper(allowEnvironmentOverrides bool) *viper.Viper { - v := viper.New() - - v.SetConfigType("json") - - v.AllowEmptyEnv(true) - - if allowEnvironmentOverrides { - v.SetEnvPrefix("mm") - v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - v.AutomaticEnv() - } - - return v -} - // marshalConfig converts the given configuration into JSON bytes for persistence. func marshalConfig(cfg *model.Config) ([]byte, error) { return json.MarshalIndent(cfg, "", " ") } - -// unmarshalConfig unmarshals a raw configuration into a Config model and environment variable overrides. -func unmarshalConfig(r io.Reader, allowEnvironmentOverrides bool) (*model.Config, map[string]interface{}, error) { - // Pre-flight check the syntax of the configuration file to improve error messaging. - configData, err := ioutil.ReadAll(r) - if err != nil { - return nil, nil, errors.Wrapf(err, "failed to read") - } - - var rawConfig model.Config - if err = json.Unmarshal(configData, &rawConfig); err != nil { - return nil, nil, jsonutils.HumanizeJsonError(err, configData) - } - rawConfig.SetDefaults() - dataWithDefaults, err := json.Marshal(rawConfig) - if err != nil { - return nil, nil, errors.Wrap(err, "failed to re-marshal config") - } - - v := newViper(allowEnvironmentOverrides) - if err := v.ReadConfig(bytes.NewReader(dataWithDefaults)); err != nil { - return nil, nil, err - } - - var config model.Config - unmarshalErr := v.Unmarshal(&config) - // https://github.com/spf13/viper/issues/324 - // https://github.com/spf13/viper/issues/348 - if unmarshalErr == nil { - config.PluginSettings.Plugins = make(map[string]map[string]interface{}) - unmarshalErr = v.UnmarshalKey("pluginsettings.plugins", &config.PluginSettings.Plugins) - } - if unmarshalErr == nil { - config.PluginSettings.PluginStates = make(map[string]*model.PluginState) - unmarshalErr = v.UnmarshalKey("pluginsettings.pluginstates", &config.PluginSettings.PluginStates) - } - - envConfig := v.EnvSettings() - - var envErr error - if envConfig, envErr = fixEnvSettingsCase(envConfig); envErr != nil { - return nil, nil, envErr - } - - return &config, envConfig, unmarshalErr -} - -// Fixes the case of the environment variables sent back from Viper since Viper stores everything -// as lower case. -func fixEnvSettingsCase(in map[string]interface{}) (out map[string]interface{}, err error) { - defer func() { - if r := recover(); r != nil { - mlog.Error("Panicked in fixEnvSettingsCase. This should never happen.", mlog.Any("err", r)) - out = in - } - }() - - var fixCase func(map[string]interface{}, reflect.Type) map[string]interface{} - fixCase = func(in map[string]interface{}, t reflect.Type) map[string]interface{} { - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - - if t.Kind() != reflect.Struct { - // Should never hit this, but this will prevent a panic if that does happen somehow - return nil - } - - fixCaseOut := make(map[string]interface{}, len(in)) - - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) - - key := field.Name - if value, ok := in[strings.ToLower(key)]; ok { - if valueAsMap, ok := value.(map[string]interface{}); ok { - fixCaseOut[key] = fixCase(valueAsMap, field.Type) - } else { - fixCaseOut[key] = value - } - } - } - - return fixCaseOut - } - - out = fixCase(in, reflect.TypeOf(model.Config{})) - - return -} diff --git a/config/unmarshal_test.go b/config/unmarshal_test.go deleted file mode 100644 index bde4e27de6..0000000000 --- a/config/unmarshal_test.go +++ /dev/null @@ -1,354 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package config - -import ( - "bytes" - "os" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/v5/model" - "github.com/mattermost/mattermost-server/v5/utils" -) - -func TestUnmarshalConfig(t *testing.T) { - _, _, err := unmarshalConfig(bytes.NewReader([]byte(``)), false) - require.EqualError(t, err, "parsing error at line 1, character 1: unexpected end of JSON input") - - _, _, err = unmarshalConfig(bytes.NewReader([]byte(` - { - malformed - `)), false) - require.EqualError(t, err, "parsing error at line 3, character 5: invalid character 'm' looking for beginning of object key string") -} - -func TestUnmarshalConfig_PluginSettings(t *testing.T) { - config, _, err := unmarshalConfig(bytes.NewReader([]byte(`{ - "PluginSettings": { - "Directory": "/temp/mattermost-plugins", - "Plugins": { - "com.example.plugin": { - "number": 1, - "string": "abc", - "boolean": false, - "abc.def.ghi": { - "abc": 123, - "def": "456" - } - }, - "jira": { - "number": 2, - "string": "123", - "boolean": true, - "abc.def.ghi": { - "abc": 456, - "def": "123" - } - } - }, - "PluginStates": { - "com.example.plugin": { - "enable": true - }, - "jira": { - "enable": false - } - } - } - }`)), false) - require.Nil(t, err) - - assert.Equal(t, "/temp/mattermost-plugins", *config.PluginSettings.Directory) - - if assert.Contains(t, config.PluginSettings.Plugins, "com.example.plugin") { - assert.Equal(t, map[string]interface{}{ - "number": float64(1), - "string": "abc", - "boolean": false, - "abc.def.ghi": map[string]interface{}{ - "abc": float64(123), - "def": "456", - }, - }, config.PluginSettings.Plugins["com.example.plugin"]) - } - if assert.Contains(t, config.PluginSettings.PluginStates, "com.example.plugin") { - assert.Equal(t, model.PluginState{ - Enable: true, - }, *config.PluginSettings.PluginStates["com.example.plugin"]) - } - - if assert.Contains(t, config.PluginSettings.Plugins, "jira") { - assert.Equal(t, map[string]interface{}{ - "number": float64(2), - "string": "123", - "boolean": true, - "abc.def.ghi": map[string]interface{}{ - "abc": float64(456), - "def": "123", - }, - }, config.PluginSettings.Plugins["jira"]) - } - if assert.Contains(t, config.PluginSettings.PluginStates, "jira") { - assert.Equal(t, model.PluginState{ - Enable: false, - }, *config.PluginSettings.PluginStates["jira"]) - } -} - -func TestConfigFromEnviroVars(t *testing.T) { - config := `{ - "ServiceSettings": { - "EnableCommands": true, - "ReadTimeout": 100 - }, - "TeamSettings": { - "SiteName": "Mattermost", - "CustomBrandText": "" - }, - "SupportSettings": { - "TermsOfServiceLink": "https://about.mattermost.com/default-terms/" - }, - "PluginSettings": { - "Enable": true, - "Plugins": { - "jira": { - "enabled": "true", - "secret": "config-secret" - } - }, - "PluginStates": { - "jira": { - "Enable": true - } - } - } - }` - - t.Run("string settings", func(t *testing.T) { - os.Setenv("MM_TEAMSETTINGS_SITENAME", "From Environment") - os.Setenv("MM_TEAMSETTINGS_CUSTOMBRANDTEXT", "Custom Brand") - - cfg, envCfg, err := unmarshalConfig(strings.NewReader(config), true) - require.Nil(t, err) - - assert.Equal(t, "From Environment", *cfg.TeamSettings.SiteName) - assert.Equal(t, "Custom Brand", *cfg.TeamSettings.CustomBrandText) - - teamSettings, ok := envCfg["TeamSettings"] - require.True(t, ok, "TeamSettings is missing from envConfig") - - teamSettingsAsMap, ok := teamSettings.(map[string]interface{}) - require.True(t, ok, "TeamSettings is not a map in envConfig") - - siteNameInEnv, ok := teamSettingsAsMap["SiteName"].(bool) - require.True(t, ok || siteNameInEnv, "SiteName should be in envConfig") - - customBrandTextInEnv, ok := teamSettingsAsMap["CustomBrandText"].(bool) - require.True(t, ok || customBrandTextInEnv, "SiteName should be in envConfig") - - os.Unsetenv("MM_TEAMSETTINGS_SITENAME") - os.Unsetenv("MM_TEAMSETTINGS_CUSTOMBRANDTEXT") - - cfg, envCfg, err = unmarshalConfig(strings.NewReader(config), true) - require.Nil(t, err) - - assert.Equal(t, "Mattermost", *cfg.TeamSettings.SiteName) - - _, ok = envCfg["TeamSettings"] - require.False(t, ok, "TeamSettings should be missing from envConfig") - }) - - t.Run("boolean setting", func(t *testing.T) { - os.Setenv("MM_SERVICESETTINGS_ENABLECOMMANDS", "false") - defer os.Unsetenv("MM_SERVICESETTINGS_ENABLECOMMANDS") - - cfg, envCfg, err := unmarshalConfig(strings.NewReader(config), true) - require.Nil(t, err) - - require.False(t, *cfg.ServiceSettings.EnableCommands, "Couldn't read config from environment var") - - serviceSettings, ok := envCfg["ServiceSettings"] - require.True(t, ok, "ServiceSettings is missing from envConfig") - - serviceSettingsAsMap, ok := serviceSettings.(map[string]interface{}) - require.True(t, ok, "ServiceSettings is not a map in envConfig") - - enableCommandsInEnv, ok := serviceSettingsAsMap["EnableCommands"].(bool) - require.True(t, ok || enableCommandsInEnv, "EnableCommands should be in envConfig") - }) - - t.Run("integer setting", func(t *testing.T) { - os.Setenv("MM_SERVICESETTINGS_READTIMEOUT", "400") - defer os.Unsetenv("MM_SERVICESETTINGS_READTIMEOUT") - - cfg, envCfg, err := unmarshalConfig(strings.NewReader(config), true) - require.Nil(t, err) - - assert.Equal(t, 400, *cfg.ServiceSettings.ReadTimeout) - - serviceSettings, ok := envCfg["ServiceSettings"] - require.True(t, ok, "ServiceSettings is missing from envConfig") - - serviceSettingsAsMap, ok := serviceSettings.(map[string]interface{}) - require.True(t, ok, "ServiceSettings is not a map in envConfig") - - readTimeoutInEnv, ok := serviceSettingsAsMap["ReadTimeout"].(bool) - require.True(t, ok || readTimeoutInEnv, "ReadTimeout should be in envConfig") - }) - - t.Run("setting missing from config.json", func(t *testing.T) { - os.Setenv("MM_SERVICESETTINGS_SITEURL", "https://example.com") - defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") - - cfg, envCfg, err := unmarshalConfig(strings.NewReader(config), true) - require.Nil(t, err) - - assert.Equal(t, "https://example.com", *cfg.ServiceSettings.SiteURL) - - serviceSettings, ok := envCfg["ServiceSettings"] - require.True(t, ok, "ServiceSettings is missing from envConfig") - - serviceSettingsAsMap, ok := serviceSettings.(map[string]interface{}) - require.True(t, ok, "ServiceSettings is not a map in envConfig") - - siteURLInEnv, ok := serviceSettingsAsMap["SiteURL"].(bool) - require.True(t, ok || siteURLInEnv, "SiteURL should be in envConfig") - }) - - t.Run("empty string setting", func(t *testing.T) { - os.Setenv("MM_SUPPORTSETTINGS_TERMSOFSERVICELINK", "") - defer os.Unsetenv("MM_SUPPORTSETTINGS_TERMSOFSERVICELINK") - - cfg, envCfg, err := unmarshalConfig(strings.NewReader(config), true) - require.Nil(t, err) - - assert.Empty(t, *cfg.SupportSettings.TermsOfServiceLink) - - supportSettings, ok := envCfg["SupportSettings"] - require.True(t, ok, "SupportSettings is missing from envConfig") - - supportSettingsAsMap, ok := supportSettings.(map[string]interface{}) - require.True(t, ok, "SupportSettings is not a map in envConfig") - - termsOfServiceLinkInEnv, ok := supportSettingsAsMap["TermsOfServiceLink"].(bool) - require.True(t, ok || termsOfServiceLinkInEnv, "TermsOfServiceLink should be in envConfig") - }) - - t.Run("feature flag", func(t *testing.T) { - os.Setenv("MM_FEATUREFLAGS_TESTFEATURE", "value") - defer os.Unsetenv("MM_FEATUREFLAGS_TESTFEATURE") - - cfg, envCfg, err := unmarshalConfig(strings.NewReader(config), true) - require.Nil(t, err) - require.NotNil(t, cfg.FeatureFlags) - - assert.Equal(t, "value", cfg.FeatureFlags.TestFeature) - - featureFlags, ok := envCfg["FeatureFlags"] - require.True(t, ok, "FeatureFlags is missing from envConfig") - - featureFlagsAsMap, ok := featureFlags.(map[string]interface{}) - require.True(t, ok, "FeatureFlags is not a map in envConfig") - - testFeatureInEnv, ok := featureFlagsAsMap["TestFeature"].(bool) - require.True(t, ok || testFeatureInEnv, "TestFeature should be in envConfig") - }) - - t.Run("plugin directory settings", func(t *testing.T) { - os.Setenv("MM_PLUGINSETTINGS_ENABLE", "false") - os.Setenv("MM_PLUGINSETTINGS_DIRECTORY", "/temp/plugins") - os.Setenv("MM_PLUGINSETTINGS_CLIENTDIRECTORY", "/temp/clientplugins") - defer os.Unsetenv("MM_PLUGINSETTINGS_ENABLE") - defer os.Unsetenv("MM_PLUGINSETTINGS_DIRECTORY") - defer os.Unsetenv("MM_PLUGINSETTINGS_CLIENTDIRECTORY") - - cfg, envCfg, err := unmarshalConfig(strings.NewReader(config), true) - require.Nil(t, err) - - assert.Equal(t, false, *cfg.PluginSettings.Enable) - assert.Equal(t, "/temp/plugins", *cfg.PluginSettings.Directory) - assert.Equal(t, "/temp/clientplugins", *cfg.PluginSettings.ClientDirectory) - - pluginSettings, ok := envCfg["PluginSettings"] - require.True(t, ok, "PluginSettings is missing from envConfig") - - pluginSettingsAsMap, ok := pluginSettings.(map[string]interface{}) - require.True(t, ok, "PluginSettings is not a map in envConfig") - - directory, ok := pluginSettingsAsMap["Directory"].(bool) - require.True(t, ok || directory, "Directory should be in envConfig") - - clientDirectory, ok := pluginSettingsAsMap["ClientDirectory"].(bool) - require.True(t, ok || clientDirectory, "ClientDirectory should be in envConfig") - }) - - t.Run("plugin specific settings cannot be overridden via environment", func(t *testing.T) { - os.Setenv("MM_PLUGINSETTINGS_PLUGINS_JIRA_ENABLED", "false") - os.Setenv("MM_PLUGINSETTINGS_PLUGINS_JIRA_SECRET", "env-secret") - os.Setenv("MM_PLUGINSETTINGS_PLUGINSTATES_JIRA_ENABLE", "false") - defer os.Unsetenv("MM_PLUGINSETTINGS_PLUGINS_JIRA_ENABLED") - defer os.Unsetenv("MM_PLUGINSETTINGS_PLUGINS_JIRA_SECRET") - defer os.Unsetenv("MM_PLUGINSETTINGS_PLUGINSTATES_JIRA_ENABLE") - - cfg, envCfg, err := unmarshalConfig(strings.NewReader(config), true) - require.Nil(t, err) - - pluginsJira, ok := cfg.PluginSettings.Plugins["jira"] - require.True(t, ok, "PluginSettings.Plugins.jira is missing from config") - - enabled, ok := pluginsJira["enabled"] - require.True(t, ok, "PluginSettings.Plugins.jira.enabled is missing from config") - assert.Equal(t, "true", enabled) - - secret, ok := pluginsJira["secret"] - require.True(t, ok, "PluginSettings.Plugins.jira.secret is missing from config") - assert.Equal(t, "config-secret", secret) - - pluginStatesJira, ok := cfg.PluginSettings.PluginStates["jira"] - require.True(t, ok, "PluginSettings.PluginStates.jira is missing from config") - require.Equal(t, true, pluginStatesJira.Enable) - - pluginSettings, ok := envCfg["PluginSettings"] - require.True(t, ok, "PluginSettings is missing from envConfig") - - pluginSettingsAsMap, ok := pluginSettings.(map[string]interface{}) - require.True(t, ok, "PluginSettings is not a map in envConfig") - - plugins, ok := pluginSettingsAsMap["Plugins"].(map[string]interface{}) - require.True(t, ok, "PluginSettings.Plugins is not a map in envConfig") - - _, ok = plugins["jira"].(map[string]interface{}) - require.False(t, ok, "PluginSettings.Plugins.jira should not be a map in envConfig") - - pluginStates, ok := pluginSettingsAsMap["PluginStates"].(map[string]interface{}) - require.True(t, ok, "PluginSettings.PluginStates is missing from envConfig") - - _, ok = pluginStates["jira"].(map[string]interface{}) - require.False(t, ok, "PluginSettings.PluginStates.jira should not be a map in envConfig") - }) -} - -func TestReadConfig_ImageProxySettings(t *testing.T) { - utils.TranslationsPreInit() - - t.Run("deprecated settings should still be read properly", func(t *testing.T) { - config, _, err := unmarshalConfig(bytes.NewReader([]byte(`{ - "ServiceSettings": { - "ImageProxyType": "OldImageProxyType", - "ImageProxyURL": "OldImageProxyURL", - "ImageProxyOptions": "OldImageProxyOptions" - } - }`)), false) - - require.Nil(t, err) - - assert.Equal(t, model.NewString("OldImageProxyType"), config.ServiceSettings.DEPRECATED_DO_NOT_USE_ImageProxyType) - assert.Equal(t, model.NewString("OldImageProxyURL"), config.ServiceSettings.DEPRECATED_DO_NOT_USE_ImageProxyURL) - assert.Equal(t, model.NewString("OldImageProxyOptions"), config.ServiceSettings.DEPRECATED_DO_NOT_USE_ImageProxyOptions) - }) -} diff --git a/config/utils.go b/config/utils.go index ae64290ea4..7e7998f86f 100644 --- a/config/utils.go +++ b/config/utils.go @@ -76,6 +76,10 @@ func desanitize(actual, target *model.Config) { if target.ServiceSettings.GfycatApiSecret != nil && *target.ServiceSettings.GfycatApiSecret == model.FAKE_SETTING { *target.ServiceSettings.GfycatApiSecret = *actual.ServiceSettings.GfycatApiSecret } + + if *target.ServiceSettings.SplitKey == model.FAKE_SETTING { + *target.ServiceSettings.SplitKey = *actual.ServiceSettings.SplitKey + } } // fixConfig patches invalid or missing data in the configuration, returning true if changed. diff --git a/config/utils_test.go b/config/utils_test.go index fbb5667bec..08f2c54415 100644 --- a/config/utils_test.go +++ b/config/utils_test.go @@ -71,6 +71,7 @@ func TestDesanitize(t *testing.T) { assert.Equal(t, *actual.ElasticsearchSettings.Password, *target.ElasticsearchSettings.Password) assert.Equal(t, actual.SqlSettings.DataSourceReplicas, target.SqlSettings.DataSourceReplicas) assert.Equal(t, actual.SqlSettings.DataSourceSearchReplicas, target.SqlSettings.DataSourceSearchReplicas) + assert.Equal(t, actual.ServiceSettings.SplitKey, target.ServiceSettings.SplitKey) } func TestFixInvalidLocales(t *testing.T) { diff --git a/go.mod b/go.mod index 4b9519e3ac..7acdcd35d9 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,6 @@ require ( github.com/klauspost/compress v1.11.1 // indirect github.com/ledongthuc/pdf v0.0.0-20200323191019-23c5852adbd2 github.com/lib/pq v1.8.0 - github.com/magiconair/properties v1.8.4 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattermost/go-i18n v1.11.0 github.com/mattermost/gorp v1.6.2-0.20200624165429-2595d5e54111 @@ -71,14 +70,12 @@ require ( github.com/mattermost/ldap v0.0.0-20191128190019-9f62ba4b8d4d github.com/mattermost/logr v1.0.13 github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 - github.com/mattermost/viper v1.0.4 github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/mholt/archiver/v3 v3.4.0 github.com/miekg/dns v1.1.31 // indirect github.com/minio/minio-go/v7 v7.0.5 github.com/mitchellh/go-testing-interface v1.14.1 // indirect - github.com/mitchellh/mapstructure v1.3.3 // indirect github.com/muesli/smartcrop v0.3.0 // indirect github.com/oklog/run v1.1.0 // indirect github.com/olivere/elastic v6.2.35+incompatible // indirect @@ -100,11 +97,9 @@ require ( github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 // indirect github.com/sirupsen/logrus v1.7.0 github.com/smartystreets/assertions v1.0.0 // indirect - github.com/spf13/afero v1.4.1 // indirect - github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cobra v1.0.0 - github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/splitio/go-client/v6 v6.0.0 github.com/stretchr/objx v0.3.0 // indirect github.com/stretchr/testify v1.6.1 github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect diff --git a/go.sum b/go.sum index 1caaa4a335..02d98f5b69 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,7 @@ code.sajari.com/docconv v1.1.1-0.20200701232649-d9ea05fbd50a h1:e0dRHyAfGcext3/l code.sajari.com/docconv v1.1.1-0.20200701232649-d9ea05fbd50a/go.mod h1:DooS873W9YwUjTwEYGpg55aDlvnx1VcEdr7IJ9rEW8g= contrib.go.opencensus.io/exporter/ocagent v0.4.9/go.mod h1:ueLzZcP7LPhPulEBukGn4aLh7Mx9YJwpVJ9nL2FYltw= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= @@ -18,6 +19,7 @@ github.com/Azure/azure-sdk-for-go v26.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo github.com/Azure/go-autorest v11.5.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 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/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -94,9 +96,7 @@ github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZw github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -181,9 +181,10 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 h1:AqeKSZIG/NIC75MNQlPy/LM3LxfpLwahICJBHwSMFNc= github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFauPHz7+NnjR/yHJGhrKo1Za+zStgwUETx3yzqgY= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/die-net/lrucache v0.0.0-20181227122439-19a39ef22a11/go.mod h1:ew0MSjCVDdtGMjF3kzLK9hwdgF5mOE8SbYVF3Rc7mkU= -github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA= github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= @@ -202,10 +203,8 @@ github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaB github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473 h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= @@ -215,7 +214,6 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqL github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 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= @@ -235,7 +233,6 @@ github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2 github.com/frankban/quicktest v1.4.0/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ= github.com/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -249,19 +246,18 @@ github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573/go.mod h1:eBvb3i github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2 h1:Ujru1hufTHVb++eG6OuNDKMxZnGIvF6o/u8q/8h2+I4= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a h1:FQqoVvjbiUioBBFUL5up+h+GdCa/AnJsL/1bIs/veSI= github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 h1:gclg6gY70GLy3PbkQ1AERPfmLMMagS60DKF78eWwLn8= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= -github.com/go-asn1-ber/asn1-ber v1.3.2-0.20191121212151-29be175fc3a3 h1:QW2p25fGTu/S0MvEftCo3wV7aEFHBt2m1DTg1HUwh+o= github.com/go-asn1-ber/asn1-ber v1.3.2-0.20191121212151-29be175fc3a3/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8= github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= @@ -269,7 +265,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= -github.com/go-resty/resty/v2 v2.0.0 h1:9Nq/U+V4xsoDnDa/iTrABDWUCuk3Ne92XFHPe6dKWUc= +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-resty/resty/v2 v2.0.0/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So= github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= @@ -286,7 +283,6 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -294,11 +290,8 @@ github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4er github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= @@ -306,13 +299,11 @@ github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -325,10 +316,9 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= @@ -338,7 +328,6 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -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= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -346,7 +335,6 @@ github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw= github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -364,7 +352,6 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA 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/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc h1:f8eY6cV/x1x+HLjOp4r72s/31/V2aTUtg5oKRRPf8/Q= github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -382,7 +369,6 @@ github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026 h1:BpJ2o0OR5FV7vrkDYf github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026/go.mod h1:5Scbynm8dF1XAPwIwkGPqzkM/shndPm79Jd1003hTjE= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -391,15 +377,12 @@ github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.0 h1:8exGP7ego3OmkfksihtSouGMZ+hQrhxx+FVELeXpVPE= github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs= github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= -github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= @@ -411,19 +394,15 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX 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/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.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 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 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/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= @@ -434,7 +413,6 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20200609203250-aecfd211c9ce h1:7UnVY3T/ZnHUrfviiAgIUjg2PXxsQfs5bphsG8F7Keo= github.com/hashicorp/yamux v0.0.0-20200609203250-aecfd211c9ce/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428 h1:Mo9W14pwbO9VfRe+ygqZ8dFbPpoIK1HFrG/zjTuQ+nc= @@ -463,9 +441,7 @@ github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= 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/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jonboulle/clockwork v0.2.0 h1:J2SLSdy7HgElq8ekSl2Mxh6vrRNFxqbXGenYH2I02Vs= github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= @@ -475,7 +451,6 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -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/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -498,32 +473,25 @@ github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0 github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.10.11 h1:K9z59aO18Aywg2b/WSgBaUX99mHy2BES18Cr5lBKZHk= github.com/klauspost/compress v1.10.11/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.1 h1:bPb7nMRdOZYDrpPMTA3EInUQrdgoBinqUuSwlGdKDdE= github.com/klauspost/compress v1.11.1/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs= github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= github.com/klauspost/pgzip v1.2.4 h1:TQ7CNpYKovDOmqzRHKxJh0BeaBI7UdQZYc6p7pMQh1A= github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -537,7 +505,6 @@ github.com/ledongthuc/pdf v0.0.0-20200323191019-23c5852adbd2 h1:H9HhyvygtvWnn1R8 github.com/ledongthuc/pdf v0.0.0-20200323191019-23c5852adbd2/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/levigross/exp-html v0.0.0-20120902181939-8df60c69a8f5 h1:W7p+m/AECTL3s/YR5RpQ4hz5SjNeKzZBl1q36ws12s0= github.com/levigross/exp-html v0.0.0-20120902181939-8df60c69a8f5/go.mod h1:QMe2wuKJ0o7zIVE8AqiT8rd8epmm6WDIZ2wyuBqYPzM= -github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= @@ -546,10 +513,7 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= -github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -566,11 +530,8 @@ github.com/mattermost/logr v1.0.13 h1:6F/fM3csvH6Oy5sUpJuW7YyZSzZZAhJm5VcgKMxA2P github.com/mattermost/logr v1.0.13/go.mod h1:Mt4DPu1NXMe6JxPdwCC0XBoxXmN9eXOIRPoZarU2PXs= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 h1:G9tL6JXRBMzjuD1kkBtcnd42kUiT6QDwxfFYu7adM6o= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs= -github.com/mattermost/viper v1.0.4 h1:cMYOz4PhguscGSPxrSokUtib5HrG4gCpiUh27wyA3d0= -github.com/mattermost/viper v1.0.4/go.mod h1:uc5hKG9lv4/KRwPOt2c1omOyirS/UnuA2TytiZQSFHM= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -579,7 +540,6 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= @@ -624,10 +584,7 @@ github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUb github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8= -github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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= @@ -635,11 +592,9 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN 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/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= -github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae h1:VeRdUYdCw49yizlSbMEn2SZ+gT+3IUKx8BqxyQdz+BY= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= -github.com/muesli/smartcrop v0.2.1-0.20181030220600-548bbf0c0965 h1:BdOnvj+P+06ZFwYd07iFWXHPfRyrJd5sAXpX9+E8bxM= github.com/muesli/smartcrop v0.2.1-0.20181030220600-548bbf0c0965/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc= github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= @@ -664,7 +619,6 @@ github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWk github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= -github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= @@ -676,28 +630,25 @@ github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FW github.com/olivere/elastic v6.2.35+incompatible h1:MMklYDy2ySi01s123CB2WLBuDMzFX4qhFcA5tKWJPgM= github.com/olivere/elastic v6.2.35+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGeB5G1iqDKVBWLNSYW8yfJ8= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.13.0 h1:M76yO2HkZASFjXL0HSoZJ1AYEmQxNJmY41Jx1zNUq1Y= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= -github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +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/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= 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/oov/psd v0.0.0-20201002182931-74231384897f h1:2LLl7BQe9ShUGGbR21vABLAPvlEF2mHY7N1zQooQRuw= github.com/oov/psd v0.0.0-20201002182931-74231384897f/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= 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= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= @@ -714,15 +665,12 @@ github.com/otiai10/gosseract/v2 v2.2.4/go.mod h1:ahOp/kHojnOMGv1RaUnR0jwY5JVa6BY github.com/otiai10/mint v1.3.0 h1:Ady6MKVezQwHBkGzLFbrsywyp09Ah7rkmfjV3Bcr5uc= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= @@ -739,21 +687,17 @@ github.com/pierrec/lz4/v3 v3.3.2/go.mod h1:280XNCGS8jAcG++AHdd6SeWnzyJ1w9oow2vbO github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 h1:D+CiwcpGTW6pL6bv6KI3KbyEyCKyS+1JWS2h8PNDnGA= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= @@ -761,37 +705,28 @@ github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f h1:BVwpUVJDADN2ufcGik7W992pyps0wZ888b/y9GXcLTU= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= -github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.14.0 h1:RHRyE8UocrbjU+6UvRzwi6HjiDfxrrBU91TtbKzkGp4= github.com/prometheus/common v0.14.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1 h1:/K3IL0Z1quvmJ7X0A1AwNEK7CRkVK3YwfOU/QAL4WGg= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= @@ -850,12 +785,9 @@ github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go. github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= -github.com/simplereach/timeutils v1.2.0 h1:btgOAlu9RW6de2r2qQiONhjgxdAG7BL6je0G6J/yPnA= github.com/simplereach/timeutils v1.2.0/go.mod h1:VVbQDfN/FHRZa1LSqcwo4kNZ62OOyqLLGQKYB3pB0Q8= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -871,27 +803,24 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:Udh github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.4.1 h1:asw9sl74539yqavKaglDM5hFpdJVK0Y5Dr/JOgQ89nQ= -github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 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/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/splitio/go-client/v6 v6.0.0 h1:HrKTYOUVVqRE/VRwmlkBOdheuXlSdiMTty/y0efp8vM= +github.com/splitio/go-client/v6 v6.0.0/go.mod h1:dTs/FpMfHb01sLso64uvudHFItiAW59ypqsK2Pbhuq4= +github.com/splitio/go-split-commons/v2 v2.0.0 h1:OJ7A4HB593GVAbBn+5/aJ19IikNL4r9pOsuFC/0JbKc= +github.com/splitio/go-split-commons/v2 v2.0.0/go.mod h1:iJVknIQ96Ezic+5FJ4vHYdqDfOV5w7s+w2wKjwgjFB0= +github.com/splitio/go-toolkit/v3 v3.0.0 h1:aSLRWLFaOioGm6BgT4iGxZlZ74qSOAq7u8HW5wwv6oc= +github.com/splitio/go-toolkit/v3 v3.0.0/go.mod h1:HGgawLnM2RlM84zVRbATpPMjF7H6u9CUYG6RlpwOlOk= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/steveyen/gtreap v0.1.0 h1:CjhzTa274PyJLJuMZwIzCO1PfC00oRa8d1Kc78bFXJM= @@ -900,16 +829,12 @@ github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3 github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -920,7 +845,6 @@ github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzH github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= github.com/throttled/throttled v2.2.5+incompatible h1:65UB52X0qNTYiT0Sohp8qLYVFwZQPDw85uSa65OljjQ= github.com/throttled/throttled v2.2.5+incompatible/go.mod h1:0BjlrEGQmvxps+HuXLsyRdqpSRvJpq0PNIsOtqP9Nos= -github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU= github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ= github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= @@ -934,10 +858,8 @@ github.com/uber/jaeger-client-go v2.25.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMW github.com/uber/jaeger-lib v2.4.0+incompatible h1:fY7QsGQWiCt8pajv4r7JEvmATdCVaWxXbjwyYwsNaLQ= github.com/uber/jaeger-lib v2.4.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -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/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4= @@ -954,7 +876,6 @@ github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMI github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 h1:d71/KA0LhvkrJ/Ok+Wx9qK7bU8meKA1Hk0jpVI5kJjk= github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1/go.mod h1:xlngVLeyQ/Qi05oQxhQ+oTuqa03RjMwMfk/7/TCs+QI= -github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= @@ -964,7 +885,6 @@ github.com/wiggin77/merror v1.0.2 h1:V0nH9eFp64ASyaXC+pB5WpvBoCg7NUwvaCSKdzlcHqw github.com/wiggin77/merror v1.0.2/go.mod h1:uQTcIU0Z6jRK4OwqganPYerzQxSFJ4GSHM3aurxxQpg= github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8= github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls= -github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= @@ -995,20 +915,19 @@ go.opencensus.io v0.19.2/go.mod h1:NO/8qkisMZLZ1FCsKNqtJPwc8/TaclWyY0B6wcYNg9M= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.22.2/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= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= @@ -1021,24 +940,24 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925 h1:5XVKs2rlCg8EFyRcvO8/XFwYxh1oKJO1Q3X5vttIf9c= +golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= -golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f h1:FO4MZ3N56GnxbqxGKqh+YTzUWQ2sDwtFQEZgLOxh9Jc= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1048,15 +967,15 @@ golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1090,7 +1009,6 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M= @@ -1103,13 +1021,9 @@ golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1125,9 +1039,9 @@ golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1139,6 +1053,7 @@ golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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= @@ -1152,16 +1067,12 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w 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-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201007165808-a893ed343c85 h1:v7tXcN5Dmvk08x9LWujjDQbk/26sd3IqhKa1NfaKmpM= golang.org/x/sys v0.0.0-20201007165808-a893ed343c85/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -1192,11 +1103,11 @@ golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20201008025239-9df69603baec h1:RY2OghEV/7X1MLaecgm1mwFd3sGvUddm5pGVSxQvX0c= golang.org/x/tools v0.0.0-20201008025239-9df69603baec/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1209,11 +1120,8 @@ google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMt google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= @@ -1225,7 +1133,6 @@ google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk google.golang.org/genproto v0.0.0-20181219182458-5a97ab628bfb/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190321212433-e79c0c59cdb5 h1:VchCZUJA1Lkjn3FxAtLPl4GotxoGt/E8ZIm9nVqbhQ8= google.golang.org/genproto v0.0.0-20190321212433-e79c0c59cdb5/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= @@ -1238,7 +1145,6 @@ google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmE google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.19.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM= google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1256,10 +1162,8 @@ google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLY google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -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.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= @@ -1269,26 +1173,22 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= -gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= @@ -1300,14 +1200,12 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -1317,7 +1215,6 @@ honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= diff --git a/migrations/helper_test.go b/migrations/helper_test.go index fae9a3256b..917903398d 100644 --- a/migrations/helper_test.go +++ b/migrations/helper_test.go @@ -33,11 +33,7 @@ func setupTestHelper(enterprise bool) *TestHelper { store := mainHelper.GetStore() store.DropAllTables() - memoryStore, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{IgnoreEnvironmentOverrides: true}) - if err != nil { - panic("failed to initialize memory store: " + err.Error()) - } - + memoryStore := config.NewTestMemoryStore() var options []app.Option options = append(options, app.ConfigStore(memoryStore)) options = append(options, app.StoreOverride(mainHelper.Store)) diff --git a/model/config.go b/model/config.go index 82f272dc16..c7bb8c5aef 100644 --- a/model/config.go +++ b/model/config.go @@ -347,7 +347,10 @@ type ServiceSettings struct { EnableLocalMode *bool LocalModeSocketLocation *string EnableAWSMetering *bool - ThreadAutoFollow *bool `access:"experimental"` + SplitKey *string `access:"environment,write_restrictable"` + FeatureFlagSyncIntervalSeconds *int `access:"environment,write_restrictable"` + DebugSplit *bool `access:"environment,write_restrictable"` + ThreadAutoFollow *bool `access:"experimental"` } func (s *ServiceSettings) SetDefaults(isUpdate bool) { @@ -766,10 +769,21 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) { s.EnableAWSMetering = NewBool(false) } + if s.SplitKey == nil { + s.SplitKey = NewString("") + } + + if s.FeatureFlagSyncIntervalSeconds == nil { + s.FeatureFlagSyncIntervalSeconds = NewInt(30) + } + + if s.DebugSplit == nil { + s.DebugSplit = NewBool(false) + } + if s.ThreadAutoFollow == nil { s.ThreadAutoFollow = NewBool(true) } - } type ClusterSettings struct { @@ -3637,6 +3651,8 @@ func (o *Config) Sanitize() { if o.ServiceSettings.GfycatApiSecret != nil && len(*o.ServiceSettings.GfycatApiSecret) > 0 { *o.ServiceSettings.GfycatApiSecret = FAKE_SETTING } + + *o.ServiceSettings.SplitKey = FAKE_SETTING } // structToMapFilteredByTag converts a struct into a map removing those fields that has the tag passed diff --git a/services/mailservice/mail_test.go b/services/mailservice/mail_test.go index cffe819a25..1a92ca8d1f 100644 --- a/services/mailservice/mail_test.go +++ b/services/mailservice/mail_test.go @@ -27,10 +27,9 @@ import ( ) func TestMailConnectionFromConfig(t *testing.T) { - fs, err := config.NewFileStore("config.json", false) - require.Nil(t, err) + store := config.NewTestMemoryStore() + cfg := store.Get() - cfg := fs.Get() conn, err := ConnectToSMTPServer(cfg) require.Nil(t, err, "Should connect to the SMTP Server %v", err) @@ -47,10 +46,8 @@ func TestMailConnectionFromConfig(t *testing.T) { } func TestMailConnectionAdvanced(t *testing.T) { - fs, err := config.NewFileStore("config.json", false) - require.Nil(t, err) - - cfg := fs.Get() + store := config.NewTestMemoryStore() + cfg := store.Get() conn, err := ConnectToSMTPServerAdvanced( &SmtpConnectionInfo{ @@ -82,8 +79,8 @@ func TestMailConnectionAdvanced(t *testing.T) { ) require.Nil(t, err2, "Should get new SMTP client") - l, err := net.Listen("tcp", "localhost:") // emulate nc -l - require.Nil(t, err, "Should've open a network socket and listen") + l, err3 := net.Listen("tcp", "localhost:") // emulate nc -l + require.Nil(t, err3, "Should've open a network socket and listen") defer l.Close() connInfo := &SmtpConnectionInfo{ @@ -106,16 +103,16 @@ func TestMailConnectionAdvanced(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() - _, err3 := NewSMTPClientAdvanced( + _, err4 := NewSMTPClientAdvanced( ctx, conn2, utils.GetHostnameFromSiteURL(*cfg.ServiceSettings.SiteURL), connInfo, ) - require.NotNil(t, err3, "Should get a timeout get while creating a new SMTP client") - assert.Equal(t, err3.Id, "utils.mail.connect_smtp.open_tls.app_error") + require.NotNil(t, err4, "Should get a timeout get while creating a new SMTP client") + assert.Equal(t, err4.Id, "utils.mail.connect_smtp.open_tls.app_error") - _, err4 := ConnectToSMTPServerAdvanced( + _, err5 := ConnectToSMTPServerAdvanced( &SmtpConnectionInfo{ ConnectionSecurity: *cfg.EmailSettings.ConnectionSecurity, SkipCertVerification: *cfg.EmailSettings.SkipServerCertificateVerification, @@ -124,13 +121,15 @@ func TestMailConnectionAdvanced(t *testing.T) { SmtpPort: "553", }, ) - require.NotNil(t, err4, "Should not connect to the SMTP Server") + require.NotNil(t, err5, "Should not connect to the SMTP Server") } func TestSendMailUsingConfig(t *testing.T) { utils.T = utils.GetUserTranslations("en") - fs, err := config.NewFileStore("config.json", false) + fsInner, err := config.NewFileStore("config.json", false) + require.Nil(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.Nil(t, err) cfg := fs.Get() @@ -169,7 +168,9 @@ func TestSendMailUsingConfig(t *testing.T) { func TestSendMailWithEmbeddedFilesUsingConfig(t *testing.T) { utils.T = utils.GetUserTranslations("en") - fs, err := config.NewFileStore("config.json", false) + fsInner, err := config.NewFileStore("config.json", false) + require.Nil(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.Nil(t, err) cfg := fs.Get() @@ -214,7 +215,9 @@ func TestSendMailWithEmbeddedFilesUsingConfig(t *testing.T) { func TestSendMailUsingConfigAdvanced(t *testing.T) { utils.T = utils.GetUserTranslations("en") - fs, err := config.NewFileStore("config.json", false) + fsInner, err := config.NewFileStore("config.json", false) + require.Nil(t, err) + fs, err := config.NewStoreFromBacking(fsInner) require.Nil(t, err) cfg := fs.Get() diff --git a/vendor/github.com/mitchellh/mapstructure/LICENSE b/vendor/github.com/dgryski/go-rendezvous/LICENSE similarity index 94% rename from vendor/github.com/mitchellh/mapstructure/LICENSE rename to vendor/github.com/dgryski/go-rendezvous/LICENSE index f9c841a51e..22080f736a 100644 --- a/vendor/github.com/mitchellh/mapstructure/LICENSE +++ b/vendor/github.com/dgryski/go-rendezvous/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 Mitchell Hashimoto +Copyright (c) 2017-2020 Damian Gryski Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/vendor/github.com/dgryski/go-rendezvous/rdv.go b/vendor/github.com/dgryski/go-rendezvous/rdv.go new file mode 100644 index 0000000000..7a6f8203c6 --- /dev/null +++ b/vendor/github.com/dgryski/go-rendezvous/rdv.go @@ -0,0 +1,79 @@ +package rendezvous + +type Rendezvous struct { + nodes map[string]int + nstr []string + nhash []uint64 + hash Hasher +} + +type Hasher func(s string) uint64 + +func New(nodes []string, hash Hasher) *Rendezvous { + r := &Rendezvous{ + nodes: make(map[string]int, len(nodes)), + nstr: make([]string, len(nodes)), + nhash: make([]uint64, len(nodes)), + hash: hash, + } + + for i, n := range nodes { + r.nodes[n] = i + r.nstr[i] = n + r.nhash[i] = hash(n) + } + + return r +} + +func (r *Rendezvous) Lookup(k string) string { + // short-circuit if we're empty + if len(r.nodes) == 0 { + return "" + } + + khash := r.hash(k) + + var midx int + var mhash = xorshiftMult64(khash ^ r.nhash[0]) + + for i, nhash := range r.nhash[1:] { + if h := xorshiftMult64(khash ^ nhash); h > mhash { + midx = i + 1 + mhash = h + } + } + + return r.nstr[midx] +} + +func (r *Rendezvous) Add(node string) { + r.nodes[node] = len(r.nstr) + r.nstr = append(r.nstr, node) + r.nhash = append(r.nhash, r.hash(node)) +} + +func (r *Rendezvous) Remove(node string) { + // find index of node to remove + nidx := r.nodes[node] + + // remove from the slices + l := len(r.nstr) + r.nstr[nidx] = r.nstr[l] + r.nstr = r.nstr[:l] + + r.nhash[nidx] = r.nhash[l] + r.nhash = r.nhash[:l] + + // update the map + delete(r.nodes, node) + moved := r.nstr[nidx] + r.nodes[moved] = nidx +} + +func xorshiftMult64(x uint64) uint64 { + x ^= x >> 12 // a + x ^= x << 25 // b + x ^= x >> 27 // c + return x * 2685821657736338717 +} diff --git a/vendor/github.com/go-redis/redis/v8/.gitignore b/vendor/github.com/go-redis/redis/v8/.gitignore new file mode 100644 index 0000000000..b975a7b4c3 --- /dev/null +++ b/vendor/github.com/go-redis/redis/v8/.gitignore @@ -0,0 +1,3 @@ +*.rdb +testdata/*/ +.idea/ diff --git a/vendor/github.com/go-redis/redis/v8/.golangci.yml b/vendor/github.com/go-redis/redis/v8/.golangci.yml new file mode 100644 index 0000000000..2132eee96f --- /dev/null +++ b/vendor/github.com/go-redis/redis/v8/.golangci.yml @@ -0,0 +1,21 @@ +run: + concurrency: 8 + deadline: 5m + tests: false +linters: + enable-all: true + disable: + - funlen + - gochecknoglobals + - gochecknoinits + - gocognit + - goconst + - godox + - gosec + - maligned + - wsl + - gomnd + - goerr113 + - exhaustive + - gofumpt + - nestif diff --git a/vendor/github.com/go-redis/redis/v8/.prettierrc b/vendor/github.com/go-redis/redis/v8/.prettierrc new file mode 100644 index 0000000000..8b7f044ad1 --- /dev/null +++ b/vendor/github.com/go-redis/redis/v8/.prettierrc @@ -0,0 +1,4 @@ +semi: false +singleQuote: true +proseWrap: always +printWidth: 100 diff --git a/vendor/github.com/go-redis/redis/v8/.travis.yml b/vendor/github.com/go-redis/redis/v8/.travis.yml new file mode 100644 index 0000000000..adedd8df4e --- /dev/null +++ b/vendor/github.com/go-redis/redis/v8/.travis.yml @@ -0,0 +1,20 @@ +dist: xenial +language: go + +services: + - redis-server + +go: + - 1.14.x + - 1.15.x + - tip + +matrix: + allow_failures: + - go: tip + +go_import_path: github.com/go-redis/redis + +before_install: + - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- + -b $(go env GOPATH)/bin v1.28.3 diff --git a/vendor/github.com/go-redis/redis/v8/CHANGELOG.md b/vendor/github.com/go-redis/redis/v8/CHANGELOG.md new file mode 100644 index 0000000000..6c2d6944c5 --- /dev/null +++ b/vendor/github.com/go-redis/redis/v8/CHANGELOG.md @@ -0,0 +1,95 @@ +# Changelog + +> :heart: [**Uptrace.dev** - distributed traces, logs, and errors in one place](https://uptrace.dev) + +## v8 + +- Documentation at https://redis.uptrace.dev/ + +- All commands require `context.Context` as a first argument, e.g. `rdb.Ping(ctx)`. If you are not + using `context.Context` yet, the simplest option is to define global package variable + `var ctx = context.TODO()` and use it when `ctx` is required. + +- Added `redis.NewFailoverClusterClient` that supports routing read-only commands to a slave node. + +- Added `redisext.OpenTemetryHook` that adds + [Redis OpenTelemetry instrumentation](https://redis.uptrace.dev/tracing/). + +- Redis slow log support. + +- Ring uses Rendezvous Hashing by default which provides better distribution. You need to move + existing keys to a new location or keys will be inaccessible / lost. To use old hashing scheme: + +```go +import "github.com/golang/groupcache/consistenthash" + +ring := redis.NewRing(&redis.RingOptions{ + NewConsistentHash: func() { + return consistenthash.New(100, crc32.ChecksumIEEE) + }, +}) +``` + +- `ClusterOptions.MaxRedirects` default value is changed from 8 to 3. +- `Options.MaxRetries` default value is changed from 0 to 3. + +- `Cluster.ForEachNode` is renamed to `ForEachShard` for consistency with `Ring`. + +## v7.3 + +- New option `Options.Username` which causes client to use `AuthACL`. Be aware if your connection + URL contains username. + +## v7.2 + +- Existing `HMSet` is renamed to `HSet` and old deprecated `HMSet` is restored for Redis 3 users. + +## v7.1 + +- Existing `Cmd.String` is renamed to `Cmd.Text`. New `Cmd.String` implements `fmt.Stringer` + interface. + +## v7 + +- _Important_. Tx.Pipeline now returns a non-transactional pipeline. Use Tx.TxPipeline for a + transactional pipeline. +- WrapProcess is replaced with more convenient AddHook that has access to context.Context. +- WithContext now can not be used to create a shallow copy of the client. +- New methods ProcessContext, DoContext, and ExecContext. +- Client respects Context.Deadline when setting net.Conn deadline. +- Client listens on Context.Done while waiting for a connection from the pool and returns an error + when context context is cancelled. +- Add PubSub.ChannelWithSubscriptions that sends `*Subscription` in addition to `*Message` to allow + detecting reconnections. +- `time.Time` is now marshalled in RFC3339 format. `rdb.Get("foo").Time()` helper is added to parse + the time. +- `SetLimiter` is removed and added `Options.Limiter` instead. +- `HMSet` is deprecated as of Redis v4. + +## v6.15 + +- Cluster and Ring pipelines process commands for each node in its own goroutine. + +## 6.14 + +- Added Options.MinIdleConns. +- Added Options.MaxConnAge. +- PoolStats.FreeConns is renamed to PoolStats.IdleConns. +- Add Client.Do to simplify creating custom commands. +- Add Cmd.String, Cmd.Int, Cmd.Int64, Cmd.Uint64, Cmd.Float64, and Cmd.Bool helpers. +- Lower memory usage. + +## v6.13 + +- Ring got new options called `HashReplicas` and `Hash`. It is recommended to set + `HashReplicas = 1000` for better keys distribution between shards. +- Cluster client was optimized to use much less memory when reloading cluster state. +- PubSub.ReceiveMessage is re-worked to not use ReceiveTimeout so it does not lose data when timeout + occurres. In most cases it is recommended to use PubSub.Channel instead. +- Dialer.KeepAlive is set to 5 minutes by default. + +## v6.12 + +- ClusterClient got new option called `ClusterSlots` which allows to build cluster of normal Redis + Servers that don't have cluster mode enabled. See + https://godoc.org/github.com/go-redis/redis#example-NewClusterClient--ManualSetup diff --git a/vendor/github.com/go-redis/redis/v8/LICENSE b/vendor/github.com/go-redis/redis/v8/LICENSE new file mode 100644 index 0000000000..298bed9bea --- /dev/null +++ b/vendor/github.com/go-redis/redis/v8/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2013 The github.com/go-redis/redis Authors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/go-redis/redis/v8/Makefile b/vendor/github.com/go-redis/redis/v8/Makefile new file mode 100644 index 0000000000..86609c6e07 --- /dev/null +++ b/vendor/github.com/go-redis/redis/v8/Makefile @@ -0,0 +1,20 @@ +all: testdeps + go test ./... + go test ./... -short -race + go test ./... -run=NONE -bench=. -benchmem + env GOOS=linux GOARCH=386 go test ./... + golangci-lint run + +testdeps: testdata/redis/src/redis-server + +bench: testdeps + go test ./... -test.run=NONE -test.bench=. -test.benchmem + +.PHONY: all test testdeps bench + +testdata/redis: + mkdir -p $@ + wget -qO- http://download.redis.io/redis-stable.tar.gz | tar xvz --strip-components=1 -C $@ + +testdata/redis/src/redis-server: testdata/redis + cd $< && make all diff --git a/vendor/github.com/go-redis/redis/v8/README.md b/vendor/github.com/go-redis/redis/v8/README.md new file mode 100644 index 0000000000..20983a6082 --- /dev/null +++ b/vendor/github.com/go-redis/redis/v8/README.md @@ -0,0 +1,151 @@ +# Redis client for Golang + +[![Build Status](https://travis-ci.org/go-redis/redis.png?branch=master)](https://travis-ci.org/go-redis/redis) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/go-redis/redis/v8)](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc) +[![Documentation](https://img.shields.io/badge/pg-documentation-informational)](https://redis.uptrace.dev/) + +> :heart: [**Uptrace.dev** - distributed traces, logs, and errors in one place](https://uptrace.dev) + +- [Docs](https://redis.uptrace.dev) +- [Reference](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc) +- [Examples](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#pkg-examples) +- [RealWorld example app](https://github.com/uptrace/go-realworld-example-app) + +## Ecosystem + +- [redisext](https://github.com/go-redis/redisext) - tracing using OpenTelemetryHook. +- [Redis Cache](https://github.com/go-redis/cache). +- [Rate limiting](https://github.com/go-redis/redis_rate). +- [Distributed Locks](https://github.com/bsm/redislock). + +## Features + +- Redis 3 commands except QUIT, MONITOR, and SYNC. +- Automatic connection pooling with + [circuit breaker](https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern) support. +- [Pub/Sub](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#PubSub). +- [Transactions](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#example-Client-TxPipeline). +- [Pipeline](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#example-Client-Pipeline) and + [TxPipeline](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#example-Client-TxPipeline). +- [Scripting](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#Script). +- [Timeouts](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#Options). +- [Redis Sentinel](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#NewFailoverClient). +- [Redis Cluster](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#NewClusterClient). +- [Cluster of Redis Servers](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#example-NewClusterClient--ManualSetup) + without using cluster mode and Redis Sentinel. +- [Ring](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#NewRing). +- [Instrumentation](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#ex-package--Instrumentation). + +API docs: https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc. Examples: +https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#pkg-examples. + +## Installation + +go-redis requires a Go version with [Modules](https://github.com/golang/go/wiki/Modules) support and +uses import versioning. So please make sure to initialize a Go module before installing go-redis: + +```shell +go mod init github.com/my/repo +go get github.com/go-redis/redis/v8 +``` + +Import: + +```go +import "github.com/go-redis/redis/v8" +``` + +## Quickstart + +```go +import ( + "context" + "github.com/go-redis/redis/v8" +) + +var ctx = context.Background() + +func ExampleNewClient() { + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + + pong, err := rdb.Ping(ctx).Result() + fmt.Println(pong, err) + // Output: PONG +} + +func ExampleClient() { + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + err := rdb.Set(ctx, "key", "value", 0).Err() + if err != nil { + panic(err) + } + + val, err := rdb.Get(ctx, "key").Result() + if err != nil { + panic(err) + } + fmt.Println("key", val) + + val2, err := rdb.Get(ctx, "key2").Result() + if err == redis.Nil { + fmt.Println("key2 does not exist") + } else if err != nil { + panic(err) + } else { + fmt.Println("key2", val2) + } + // Output: key value + // key2 does not exist +} +``` + +## Howto + +Please go through [examples](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#pkg-examples) +to get an idea how to use this package. + +## Look and feel + +Some corner cases: + +```go +// SET key value EX 10 NX +set, err := rdb.SetNX(ctx, "key", "value", 10*time.Second).Result() + +// SORT list LIMIT 0 2 ASC +vals, err := rdb.Sort(ctx, "list", &redis.Sort{Offset: 0, Count: 2, Order: "ASC"}).Result() + +// ZRANGEBYSCORE zset -inf +inf WITHSCORES LIMIT 0 2 +vals, err := rdb.ZRangeByScoreWithScores(ctx, "zset", &redis.ZRangeBy{ + Min: "-inf", + Max: "+inf", + Offset: 0, + Count: 2, +}).Result() + +// ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3 AGGREGATE SUM +vals, err := rdb.ZInterStore(ctx, "out", &redis.ZStore{ + Keys: []string{"zset1", "zset2"}, + Weights: []int64{2, 3} +}).Result() + +// EVAL "return {KEYS[1],ARGV[1]}" 1 "key" "hello" +vals, err := rdb.Eval(ctx, "return {KEYS[1],ARGV[1]}", []string{"key"}, "hello").Result() + +// custom command +res, err := rdb.Do(ctx, "set", "key", "value").Result() +``` + +## See also + +- [Golang PostgreSQL ORM](https://github.com/go-pg/pg) +- [Golang msgpack](https://github.com/vmihailenco/msgpack) +- [Golang message task queue](https://github.com/vmihailenco/taskq) diff --git a/vendor/github.com/go-redis/redis/v8/cluster.go b/vendor/github.com/go-redis/redis/v8/cluster.go new file mode 100644 index 0000000000..b6c4daacc0 --- /dev/null +++ b/vendor/github.com/go-redis/redis/v8/cluster.go @@ -0,0 +1,1703 @@ +package redis + +import ( + "context" + "crypto/tls" + "fmt" + "math" + "net" + "runtime" + "sort" + "sync" + "sync/atomic" + "time" + + "github.com/go-redis/redis/v8/internal" + "github.com/go-redis/redis/v8/internal/hashtag" + "github.com/go-redis/redis/v8/internal/pool" + "github.com/go-redis/redis/v8/internal/proto" + "golang.org/x/exp/rand" +) + +var errClusterNoNodes = fmt.Errorf("redis: cluster has no nodes") + +// ClusterOptions are used to configure a cluster client and should be +// passed to NewClusterClient. +type ClusterOptions struct { + // A seed list of host:port addresses of cluster nodes. + Addrs []string + + // NewClient creates a cluster node client with provided name and options. + NewClient func(opt *Options) *Client + + // The maximum number of retries before giving up. Command is retried + // on network errors and MOVED/ASK redirects. + // Default is 3 retries. + MaxRedirects int + + // Enables read-only commands on slave nodes. + ReadOnly bool + // Allows routing read-only commands to the closest master or slave node. + // It automatically enables ReadOnly. + RouteByLatency bool + // Allows routing read-only commands to the random master or slave node. + // It automatically enables ReadOnly. + RouteRandomly bool + + // Optional function that returns cluster slots information. + // It is useful to manually create cluster of standalone Redis servers + // and load-balance read/write operations between master and slaves. + // It can use service like ZooKeeper to maintain configuration information + // and Cluster.ReloadState to manually trigger state reloading. + ClusterSlots func(context.Context) ([]ClusterSlot, error) + + // Following options are copied from Options struct. + + Dialer func(ctx context.Context, network, addr string) (net.Conn, error) + + OnConnect func(ctx context.Context, cn *Conn) error + + Username string + Password string + + MaxRetries int + MinRetryBackoff time.Duration + MaxRetryBackoff time.Duration + + DialTimeout time.Duration + ReadTimeout time.Duration + WriteTimeout time.Duration + + // PoolSize applies per cluster node and not for the whole cluster. + PoolSize int + MinIdleConns int + MaxConnAge time.Duration + PoolTimeout time.Duration + IdleTimeout time.Duration + IdleCheckFrequency time.Duration + + TLSConfig *tls.Config +} + +func (opt *ClusterOptions) init() { + if opt.MaxRedirects == -1 { + opt.MaxRedirects = 0 + } else if opt.MaxRedirects == 0 { + opt.MaxRedirects = 3 + } + + if (opt.RouteByLatency || opt.RouteRandomly) && opt.ClusterSlots == nil { + opt.ReadOnly = true + } + + if opt.PoolSize == 0 { + opt.PoolSize = 5 * runtime.NumCPU() + } + + switch opt.ReadTimeout { + case -1: + opt.ReadTimeout = 0 + case 0: + opt.ReadTimeout = 3 * time.Second + } + switch opt.WriteTimeout { + case -1: + opt.WriteTimeout = 0 + case 0: + opt.WriteTimeout = opt.ReadTimeout + } + + if opt.MaxRetries == 0 { + opt.MaxRetries = -1 + } + switch opt.MinRetryBackoff { + case -1: + opt.MinRetryBackoff = 0 + case 0: + opt.MinRetryBackoff = 8 * time.Millisecond + } + switch opt.MaxRetryBackoff { + case -1: + opt.MaxRetryBackoff = 0 + case 0: + opt.MaxRetryBackoff = 512 * time.Millisecond + } + + if opt.NewClient == nil { + opt.NewClient = NewClient + } +} + +func (opt *ClusterOptions) clientOptions() *Options { + const disableIdleCheck = -1 + + return &Options{ + Dialer: opt.Dialer, + OnConnect: opt.OnConnect, + + Username: opt.Username, + Password: opt.Password, + + MaxRetries: opt.MaxRetries, + MinRetryBackoff: opt.MinRetryBackoff, + MaxRetryBackoff: opt.MaxRetryBackoff, + + DialTimeout: opt.DialTimeout, + ReadTimeout: opt.ReadTimeout, + WriteTimeout: opt.WriteTimeout, + + PoolSize: opt.PoolSize, + MinIdleConns: opt.MinIdleConns, + MaxConnAge: opt.MaxConnAge, + PoolTimeout: opt.PoolTimeout, + IdleTimeout: opt.IdleTimeout, + IdleCheckFrequency: disableIdleCheck, + + readOnly: opt.ReadOnly, + + TLSConfig: opt.TLSConfig, + } +} + +//------------------------------------------------------------------------------ + +type clusterNode struct { + Client *Client + + latency uint32 // atomic + generation uint32 // atomic + failing uint32 // atomic +} + +func newClusterNode(clOpt *ClusterOptions, addr string) *clusterNode { + opt := clOpt.clientOptions() + opt.Addr = addr + node := clusterNode{ + Client: clOpt.NewClient(opt), + } + + node.latency = math.MaxUint32 + if clOpt.RouteByLatency { + go node.updateLatency() + } + + return &node +} + +func (n *clusterNode) String() string { + return n.Client.String() +} + +func (n *clusterNode) Close() error { + return n.Client.Close() +} + +func (n *clusterNode) updateLatency() { + const numProbe = 10 + var dur uint64 + + for i := 0; i < numProbe; i++ { + time.Sleep(time.Duration(10+rand.Intn(10)) * time.Millisecond) + + start := time.Now() + n.Client.Ping(context.TODO()) + dur += uint64(time.Since(start) / time.Microsecond) + } + + latency := float64(dur) / float64(numProbe) + atomic.StoreUint32(&n.latency, uint32(latency+0.5)) +} + +func (n *clusterNode) Latency() time.Duration { + latency := atomic.LoadUint32(&n.latency) + return time.Duration(latency) * time.Microsecond +} + +func (n *clusterNode) MarkAsFailing() { + atomic.StoreUint32(&n.failing, uint32(time.Now().Unix())) +} + +func (n *clusterNode) Failing() bool { + const timeout = 15 // 15 seconds + + failing := atomic.LoadUint32(&n.failing) + if failing == 0 { + return false + } + if time.Now().Unix()-int64(failing) < timeout { + return true + } + atomic.StoreUint32(&n.failing, 0) + return false +} + +func (n *clusterNode) Generation() uint32 { + return atomic.LoadUint32(&n.generation) +} + +func (n *clusterNode) SetGeneration(gen uint32) { + for { + v := atomic.LoadUint32(&n.generation) + if gen < v || atomic.CompareAndSwapUint32(&n.generation, v, gen) { + break + } + } +} + +//------------------------------------------------------------------------------ + +type clusterNodes struct { + opt *ClusterOptions + + mu sync.RWMutex + addrs []string + nodes map[string]*clusterNode + activeAddrs []string + closed bool + + _generation uint32 // atomic +} + +func newClusterNodes(opt *ClusterOptions) *clusterNodes { + return &clusterNodes{ + opt: opt, + + addrs: opt.Addrs, + nodes: make(map[string]*clusterNode), + } +} + +func (c *clusterNodes) Close() error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.closed { + return nil + } + c.closed = true + + var firstErr error + for _, node := range c.nodes { + if err := node.Client.Close(); err != nil && firstErr == nil { + firstErr = err + } + } + + c.nodes = nil + c.activeAddrs = nil + + return firstErr +} + +func (c *clusterNodes) Addrs() ([]string, error) { + var addrs []string + c.mu.RLock() + closed := c.closed + if !closed { + if len(c.activeAddrs) > 0 { + addrs = c.activeAddrs + } else { + addrs = c.addrs + } + } + c.mu.RUnlock() + + if closed { + return nil, pool.ErrClosed + } + if len(addrs) == 0 { + return nil, errClusterNoNodes + } + return addrs, nil +} + +func (c *clusterNodes) NextGeneration() uint32 { + return atomic.AddUint32(&c._generation, 1) +} + +// GC removes unused nodes. +func (c *clusterNodes) GC(generation uint32) { + //nolint:prealloc + var collected []*clusterNode + + c.mu.Lock() + + c.activeAddrs = c.activeAddrs[:0] + for addr, node := range c.nodes { + if node.Generation() >= generation { + c.activeAddrs = append(c.activeAddrs, addr) + if c.opt.RouteByLatency { + go node.updateLatency() + } + continue + } + + delete(c.nodes, addr) + collected = append(collected, node) + } + + c.mu.Unlock() + + for _, node := range collected { + _ = node.Client.Close() + } +} + +func (c *clusterNodes) Get(addr string) (*clusterNode, error) { + node, err := c.get(addr) + if err != nil { + return nil, err + } + if node != nil { + return node, nil + } + + c.mu.Lock() + defer c.mu.Unlock() + + if c.closed { + return nil, pool.ErrClosed + } + + node, ok := c.nodes[addr] + if ok { + return node, nil + } + + node = newClusterNode(c.opt, addr) + + c.addrs = appendIfNotExists(c.addrs, addr) + c.nodes[addr] = node + + return node, nil +} + +func (c *clusterNodes) get(addr string) (*clusterNode, error) { + var node *clusterNode + var err error + c.mu.RLock() + if c.closed { + err = pool.ErrClosed + } else { + node = c.nodes[addr] + } + c.mu.RUnlock() + return node, err +} + +func (c *clusterNodes) All() ([]*clusterNode, error) { + c.mu.RLock() + defer c.mu.RUnlock() + + if c.closed { + return nil, pool.ErrClosed + } + + cp := make([]*clusterNode, 0, len(c.nodes)) + for _, node := range c.nodes { + cp = append(cp, node) + } + return cp, nil +} + +func (c *clusterNodes) Random() (*clusterNode, error) { + addrs, err := c.Addrs() + if err != nil { + return nil, err + } + + n := rand.Intn(len(addrs)) + return c.Get(addrs[n]) +} + +//------------------------------------------------------------------------------ + +type clusterSlot struct { + start, end int + nodes []*clusterNode +} + +type clusterSlotSlice []*clusterSlot + +func (p clusterSlotSlice) Len() int { + return len(p) +} + +func (p clusterSlotSlice) Less(i, j int) bool { + return p[i].start < p[j].start +} + +func (p clusterSlotSlice) Swap(i, j int) { + p[i], p[j] = p[j], p[i] +} + +type clusterState struct { + nodes *clusterNodes + Masters []*clusterNode + Slaves []*clusterNode + + slots []*clusterSlot + + generation uint32 + createdAt time.Time +} + +func newClusterState( + nodes *clusterNodes, slots []ClusterSlot, origin string, +) (*clusterState, error) { + c := clusterState{ + nodes: nodes, + + slots: make([]*clusterSlot, 0, len(slots)), + + generation: nodes.NextGeneration(), + createdAt: time.Now(), + } + + originHost, _, _ := net.SplitHostPort(origin) + isLoopbackOrigin := isLoopback(originHost) + + for _, slot := range slots { + var nodes []*clusterNode + for i, slotNode := range slot.Nodes { + addr := slotNode.Addr + if !isLoopbackOrigin { + addr = replaceLoopbackHost(addr, originHost) + } + + node, err := c.nodes.Get(addr) + if err != nil { + return nil, err + } + + node.SetGeneration(c.generation) + nodes = append(nodes, node) + + if i == 0 { + c.Masters = appendUniqueNode(c.Masters, node) + } else { + c.Slaves = appendUniqueNode(c.Slaves, node) + } + } + + c.slots = append(c.slots, &clusterSlot{ + start: slot.Start, + end: slot.End, + nodes: nodes, + }) + } + + sort.Sort(clusterSlotSlice(c.slots)) + + time.AfterFunc(time.Minute, func() { + nodes.GC(c.generation) + }) + + return &c, nil +} + +func replaceLoopbackHost(nodeAddr, originHost string) string { + nodeHost, nodePort, err := net.SplitHostPort(nodeAddr) + if err != nil { + return nodeAddr + } + + nodeIP := net.ParseIP(nodeHost) + if nodeIP == nil { + return nodeAddr + } + + if !nodeIP.IsLoopback() { + return nodeAddr + } + + // Use origin host which is not loopback and node port. + return net.JoinHostPort(originHost, nodePort) +} + +func isLoopback(host string) bool { + ip := net.ParseIP(host) + if ip == nil { + return true + } + return ip.IsLoopback() +} + +func (c *clusterState) slotMasterNode(slot int) (*clusterNode, error) { + nodes := c.slotNodes(slot) + if len(nodes) > 0 { + return nodes[0], nil + } + return c.nodes.Random() +} + +func (c *clusterState) slotSlaveNode(slot int) (*clusterNode, error) { + nodes := c.slotNodes(slot) + switch len(nodes) { + case 0: + return c.nodes.Random() + case 1: + return nodes[0], nil + case 2: + if slave := nodes[1]; !slave.Failing() { + return slave, nil + } + return nodes[0], nil + default: + var slave *clusterNode + for i := 0; i < 10; i++ { + n := rand.Intn(len(nodes)-1) + 1 + slave = nodes[n] + if !slave.Failing() { + return slave, nil + } + } + + // All slaves are loading - use master. + return nodes[0], nil + } +} + +func (c *clusterState) slotClosestNode(slot int) (*clusterNode, error) { + nodes := c.slotNodes(slot) + if len(nodes) == 0 { + return c.nodes.Random() + } + + var node *clusterNode + for _, n := range nodes { + if n.Failing() { + continue + } + if node == nil || n.Latency() < node.Latency() { + node = n + } + } + if node != nil { + return node, nil + } + + // If all nodes are failing - return random node + return c.nodes.Random() +} + +func (c *clusterState) slotRandomNode(slot int) (*clusterNode, error) { + nodes := c.slotNodes(slot) + if len(nodes) == 0 { + return c.nodes.Random() + } + n := rand.Intn(len(nodes)) + return nodes[n], nil +} + +func (c *clusterState) slotNodes(slot int) []*clusterNode { + i := sort.Search(len(c.slots), func(i int) bool { + return c.slots[i].end >= slot + }) + if i >= len(c.slots) { + return nil + } + x := c.slots[i] + if slot >= x.start && slot <= x.end { + return x.nodes + } + return nil +} + +//------------------------------------------------------------------------------ + +type clusterStateHolder struct { + load func(ctx context.Context) (*clusterState, error) + + state atomic.Value + reloading uint32 // atomic +} + +func newClusterStateHolder(fn func(ctx context.Context) (*clusterState, error)) *clusterStateHolder { + return &clusterStateHolder{ + load: fn, + } +} + +func (c *clusterStateHolder) Reload(ctx context.Context) (*clusterState, error) { + state, err := c.load(ctx) + if err != nil { + return nil, err + } + c.state.Store(state) + return state, nil +} + +func (c *clusterStateHolder) LazyReload(ctx context.Context) { + if !atomic.CompareAndSwapUint32(&c.reloading, 0, 1) { + return + } + go func() { + defer atomic.StoreUint32(&c.reloading, 0) + + _, err := c.Reload(ctx) + if err != nil { + return + } + time.Sleep(200 * time.Millisecond) + }() +} + +func (c *clusterStateHolder) Get(ctx context.Context) (*clusterState, error) { + v := c.state.Load() + if v != nil { + state := v.(*clusterState) + if time.Since(state.createdAt) > 10*time.Second { + c.LazyReload(ctx) + } + return state, nil + } + return c.Reload(ctx) +} + +func (c *clusterStateHolder) ReloadOrGet(ctx context.Context) (*clusterState, error) { + state, err := c.Reload(ctx) + if err == nil { + return state, nil + } + return c.Get(ctx) +} + +//------------------------------------------------------------------------------ + +type clusterClient struct { + opt *ClusterOptions + nodes *clusterNodes + state *clusterStateHolder //nolint:structcheck + cmdsInfoCache *cmdsInfoCache //nolint:structcheck +} + +// ClusterClient is a Redis Cluster client representing a pool of zero +// or more underlying connections. It's safe for concurrent use by +// multiple goroutines. +type ClusterClient struct { + *clusterClient + cmdable + hooks + ctx context.Context +} + +// NewClusterClient returns a Redis Cluster client as described in +// http://redis.io/topics/cluster-spec. +func NewClusterClient(opt *ClusterOptions) *ClusterClient { + opt.init() + + c := &ClusterClient{ + clusterClient: &clusterClient{ + opt: opt, + nodes: newClusterNodes(opt), + }, + ctx: context.Background(), + } + c.state = newClusterStateHolder(c.loadState) + c.cmdsInfoCache = newCmdsInfoCache(c.cmdsInfo) + c.cmdable = c.Process + + if opt.IdleCheckFrequency > 0 { + go c.reaper(opt.IdleCheckFrequency) + } + + return c +} + +func (c *ClusterClient) Context() context.Context { + return c.ctx +} + +func (c *ClusterClient) WithContext(ctx context.Context) *ClusterClient { + if ctx == nil { + panic("nil context") + } + clone := *c + clone.cmdable = clone.Process + clone.hooks.lock() + clone.ctx = ctx + return &clone +} + +// Options returns read-only Options that were used to create the client. +func (c *ClusterClient) Options() *ClusterOptions { + return c.opt +} + +// ReloadState reloads cluster state. If available it calls ClusterSlots func +// to get cluster slots information. +func (c *ClusterClient) ReloadState(ctx context.Context) { + c.state.LazyReload(ctx) +} + +// Close closes the cluster client, releasing any open resources. +// +// It is rare to Close a ClusterClient, as the ClusterClient is meant +// to be long-lived and shared between many goroutines. +func (c *ClusterClient) Close() error { + return c.nodes.Close() +} + +// Do creates a Cmd from the args and processes the cmd. +func (c *ClusterClient) Do(ctx context.Context, args ...interface{}) *Cmd { + cmd := NewCmd(ctx, args...) + _ = c.Process(ctx, cmd) + return cmd +} + +func (c *ClusterClient) Process(ctx context.Context, cmd Cmder) error { + return c.hooks.process(ctx, cmd, c.process) +} + +func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error { + err := c._process(ctx, cmd) + if err != nil { + cmd.SetErr(err) + return err + } + return nil +} + +func (c *ClusterClient) _process(ctx context.Context, cmd Cmder) error { + cmdInfo := c.cmdInfo(cmd.Name()) + slot := c.cmdSlot(cmd) + + var node *clusterNode + var ask bool + var lastErr error + for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ { + if attempt > 0 { + if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { + return err + } + } + + if node == nil { + var err error + node, err = c.cmdNode(ctx, cmdInfo, slot) + if err != nil { + return err + } + } + + if ask { + pipe := node.Client.Pipeline() + _ = pipe.Process(ctx, NewCmd(ctx, "asking")) + _ = pipe.Process(ctx, cmd) + _, lastErr = pipe.Exec(ctx) + _ = pipe.Close() + ask = false + } else { + lastErr = node.Client.Process(ctx, cmd) + } + + // If there is no error - we are done. + if lastErr == nil { + return nil + } + if isReadOnly := isReadOnlyError(lastErr); isReadOnly || lastErr == pool.ErrClosed { + if isReadOnly { + c.state.LazyReload(ctx) + } + node = nil + continue + } + + // If slave is loading - pick another node. + if c.opt.ReadOnly && isLoadingError(lastErr) { + node.MarkAsFailing() + node = nil + continue + } + + var moved bool + var addr string + moved, ask, addr = isMovedError(lastErr) + if moved || ask { + var err error + node, err = c.nodes.Get(addr) + if err != nil { + return err + } + continue + } + + if shouldRetry(lastErr, cmd.readTimeout() == nil) { + // First retry the same node. + if attempt == 0 { + continue + } + + // Second try another node. + node.MarkAsFailing() + node = nil + continue + } + + return lastErr + } + return lastErr +} + +// ForEachMaster concurrently calls the fn on each master node in the cluster. +// It returns the first error if any. +func (c *ClusterClient) ForEachMaster( + ctx context.Context, + fn func(ctx context.Context, client *Client) error, +) error { + state, err := c.state.ReloadOrGet(ctx) + if err != nil { + return err + } + + var wg sync.WaitGroup + errCh := make(chan error, 1) + + for _, master := range state.Masters { + wg.Add(1) + go func(node *clusterNode) { + defer wg.Done() + err := fn(ctx, node.Client) + if err != nil { + select { + case errCh <- err: + default: + } + } + }(master) + } + + wg.Wait() + + select { + case err := <-errCh: + return err + default: + return nil + } +} + +// ForEachSlave concurrently calls the fn on each slave node in the cluster. +// It returns the first error if any. +func (c *ClusterClient) ForEachSlave( + ctx context.Context, + fn func(ctx context.Context, client *Client) error, +) error { + state, err := c.state.ReloadOrGet(ctx) + if err != nil { + return err + } + + var wg sync.WaitGroup + errCh := make(chan error, 1) + + for _, slave := range state.Slaves { + wg.Add(1) + go func(node *clusterNode) { + defer wg.Done() + err := fn(ctx, node.Client) + if err != nil { + select { + case errCh <- err: + default: + } + } + }(slave) + } + + wg.Wait() + + select { + case err := <-errCh: + return err + default: + return nil + } +} + +// ForEachShard concurrently calls the fn on each known node in the cluster. +// It returns the first error if any. +func (c *ClusterClient) ForEachShard( + ctx context.Context, + fn func(ctx context.Context, client *Client) error, +) error { + state, err := c.state.ReloadOrGet(ctx) + if err != nil { + return err + } + + var wg sync.WaitGroup + errCh := make(chan error, 1) + + worker := func(node *clusterNode) { + defer wg.Done() + err := fn(ctx, node.Client) + if err != nil { + select { + case errCh <- err: + default: + } + } + } + + for _, node := range state.Masters { + wg.Add(1) + go worker(node) + } + for _, node := range state.Slaves { + wg.Add(1) + go worker(node) + } + + wg.Wait() + + select { + case err := <-errCh: + return err + default: + return nil + } +} + +// PoolStats returns accumulated connection pool stats. +func (c *ClusterClient) PoolStats() *PoolStats { + var acc PoolStats + + state, _ := c.state.Get(context.TODO()) + if state == nil { + return &acc + } + + for _, node := range state.Masters { + s := node.Client.connPool.Stats() + acc.Hits += s.Hits + acc.Misses += s.Misses + acc.Timeouts += s.Timeouts + + acc.TotalConns += s.TotalConns + acc.IdleConns += s.IdleConns + acc.StaleConns += s.StaleConns + } + + for _, node := range state.Slaves { + s := node.Client.connPool.Stats() + acc.Hits += s.Hits + acc.Misses += s.Misses + acc.Timeouts += s.Timeouts + + acc.TotalConns += s.TotalConns + acc.IdleConns += s.IdleConns + acc.StaleConns += s.StaleConns + } + + return &acc +} + +func (c *ClusterClient) loadState(ctx context.Context) (*clusterState, error) { + if c.opt.ClusterSlots != nil { + slots, err := c.opt.ClusterSlots(ctx) + if err != nil { + return nil, err + } + return newClusterState(c.nodes, slots, "") + } + + addrs, err := c.nodes.Addrs() + if err != nil { + return nil, err + } + + var firstErr error + + for _, idx := range rand.Perm(len(addrs)) { + addr := addrs[idx] + + node, err := c.nodes.Get(addr) + if err != nil { + if firstErr == nil { + firstErr = err + } + continue + } + + slots, err := node.Client.ClusterSlots(ctx).Result() + if err != nil { + if firstErr == nil { + firstErr = err + } + continue + } + + return newClusterState(c.nodes, slots, node.Client.opt.Addr) + } + + /* + * No node is connectable. It's possible that all nodes' IP has changed. + * Clear activeAddrs to let client be able to re-connect using the initial + * setting of the addresses (e.g. [redis-cluster-0:6379, redis-cluster-1:6379]), + * which might have chance to resolve domain name and get updated IP address. + */ + c.nodes.mu.Lock() + c.nodes.activeAddrs = nil + c.nodes.mu.Unlock() + + return nil, firstErr +} + +// reaper closes idle connections to the cluster. +func (c *ClusterClient) reaper(idleCheckFrequency time.Duration) { + ticker := time.NewTicker(idleCheckFrequency) + defer ticker.Stop() + + for range ticker.C { + nodes, err := c.nodes.All() + if err != nil { + break + } + + for _, node := range nodes { + _, err := node.Client.connPool.(*pool.ConnPool).ReapStaleConns() + if err != nil { + internal.Logger.Printf(c.Context(), "ReapStaleConns failed: %s", err) + } + } + } +} + +func (c *ClusterClient) Pipeline() Pipeliner { + pipe := Pipeline{ + ctx: c.ctx, + exec: c.processPipeline, + } + pipe.init() + return &pipe +} + +func (c *ClusterClient) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) { + return c.Pipeline().Pipelined(ctx, fn) +} + +func (c *ClusterClient) processPipeline(ctx context.Context, cmds []Cmder) error { + return c.hooks.processPipeline(ctx, cmds, c._processPipeline) +} + +func (c *ClusterClient) _processPipeline(ctx context.Context, cmds []Cmder) error { + cmdsMap := newCmdsMap() + err := c.mapCmdsByNode(ctx, cmdsMap, cmds) + if err != nil { + setCmdsErr(cmds, err) + return err + } + + for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ { + if attempt > 0 { + if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { + setCmdsErr(cmds, err) + return err + } + } + + failedCmds := newCmdsMap() + var wg sync.WaitGroup + + for node, cmds := range cmdsMap.m { + wg.Add(1) + go func(node *clusterNode, cmds []Cmder) { + defer wg.Done() + + err := c._processPipelineNode(ctx, node, cmds, failedCmds) + if err == nil { + return + } + if attempt < c.opt.MaxRedirects { + if err := c.mapCmdsByNode(ctx, failedCmds, cmds); err != nil { + setCmdsErr(cmds, err) + } + } else { + setCmdsErr(cmds, err) + } + }(node, cmds) + } + + wg.Wait() + if len(failedCmds.m) == 0 { + break + } + cmdsMap = failedCmds + } + + return cmdsFirstErr(cmds) +} + +func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmds []Cmder) error { + state, err := c.state.Get(ctx) + if err != nil { + return err + } + + if c.opt.ReadOnly && c.cmdsAreReadOnly(cmds) { + for _, cmd := range cmds { + slot := c.cmdSlot(cmd) + node, err := c.slotReadOnlyNode(state, slot) + if err != nil { + return err + } + cmdsMap.Add(node, cmd) + } + return nil + } + + for _, cmd := range cmds { + slot := c.cmdSlot(cmd) + node, err := state.slotMasterNode(slot) + if err != nil { + return err + } + cmdsMap.Add(node, cmd) + } + return nil +} + +func (c *ClusterClient) cmdsAreReadOnly(cmds []Cmder) bool { + for _, cmd := range cmds { + cmdInfo := c.cmdInfo(cmd.Name()) + if cmdInfo == nil || !cmdInfo.ReadOnly { + return false + } + } + return true +} + +func (c *ClusterClient) _processPipelineNode( + ctx context.Context, node *clusterNode, cmds []Cmder, failedCmds *cmdsMap, +) error { + return node.Client.hooks.processPipeline(ctx, cmds, func(ctx context.Context, cmds []Cmder) error { + return node.Client.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error { + err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error { + return writeCmds(wr, cmds) + }) + if err != nil { + return err + } + + return cn.WithReader(ctx, c.opt.ReadTimeout, func(rd *proto.Reader) error { + return c.pipelineReadCmds(ctx, node, rd, cmds, failedCmds) + }) + }) + }) +} + +func (c *ClusterClient) pipelineReadCmds( + ctx context.Context, + node *clusterNode, + rd *proto.Reader, + cmds []Cmder, + failedCmds *cmdsMap, +) error { + for _, cmd := range cmds { + err := cmd.readReply(rd) + if err == nil { + continue + } + if c.checkMovedErr(ctx, cmd, err, failedCmds) { + continue + } + + if c.opt.ReadOnly && isLoadingError(err) { + node.MarkAsFailing() + return err + } + if isRedisError(err) { + continue + } + return err + } + return nil +} + +func (c *ClusterClient) checkMovedErr( + ctx context.Context, cmd Cmder, err error, failedCmds *cmdsMap, +) bool { + moved, ask, addr := isMovedError(err) + if !moved && !ask { + return false + } + + node, err := c.nodes.Get(addr) + if err != nil { + return false + } + + if moved { + c.state.LazyReload(ctx) + failedCmds.Add(node, cmd) + return true + } + + if ask { + failedCmds.Add(node, NewCmd(ctx, "asking"), cmd) + return true + } + + panic("not reached") +} + +// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC. +func (c *ClusterClient) TxPipeline() Pipeliner { + pipe := Pipeline{ + ctx: c.ctx, + exec: c.processTxPipeline, + } + pipe.init() + return &pipe +} + +func (c *ClusterClient) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) { + return c.TxPipeline().Pipelined(ctx, fn) +} + +func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) error { + return c.hooks.processPipeline(ctx, cmds, c._processTxPipeline) +} + +func (c *ClusterClient) _processTxPipeline(ctx context.Context, cmds []Cmder) error { + state, err := c.state.Get(ctx) + if err != nil { + setCmdsErr(cmds, err) + return err + } + + cmdsMap := c.mapCmdsBySlot(cmds) + for slot, cmds := range cmdsMap { + node, err := state.slotMasterNode(slot) + if err != nil { + setCmdsErr(cmds, err) + continue + } + + cmdsMap := map[*clusterNode][]Cmder{node: cmds} + for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ { + if attempt > 0 { + if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { + setCmdsErr(cmds, err) + return err + } + } + + failedCmds := newCmdsMap() + var wg sync.WaitGroup + + for node, cmds := range cmdsMap { + wg.Add(1) + go func(node *clusterNode, cmds []Cmder) { + defer wg.Done() + + err := c._processTxPipelineNode(ctx, node, cmds, failedCmds) + if err == nil { + return + } + if attempt < c.opt.MaxRedirects { + if err := c.mapCmdsByNode(ctx, failedCmds, cmds); err != nil { + setCmdsErr(cmds, err) + } + } else { + setCmdsErr(cmds, err) + } + }(node, cmds) + } + + wg.Wait() + if len(failedCmds.m) == 0 { + break + } + cmdsMap = failedCmds.m + } + } + + return cmdsFirstErr(cmds) +} + +func (c *ClusterClient) mapCmdsBySlot(cmds []Cmder) map[int][]Cmder { + cmdsMap := make(map[int][]Cmder) + for _, cmd := range cmds { + slot := c.cmdSlot(cmd) + cmdsMap[slot] = append(cmdsMap[slot], cmd) + } + return cmdsMap +} + +func (c *ClusterClient) _processTxPipelineNode( + ctx context.Context, node *clusterNode, cmds []Cmder, failedCmds *cmdsMap, +) error { + return node.Client.hooks.processTxPipeline(ctx, cmds, func(ctx context.Context, cmds []Cmder) error { + return node.Client.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error { + err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error { + return writeCmds(wr, cmds) + }) + if err != nil { + return err + } + + return cn.WithReader(ctx, c.opt.ReadTimeout, func(rd *proto.Reader) error { + statusCmd := cmds[0].(*StatusCmd) + // Trim multi and exec. + cmds = cmds[1 : len(cmds)-1] + + err := c.txPipelineReadQueued(ctx, rd, statusCmd, cmds, failedCmds) + if err != nil { + moved, ask, addr := isMovedError(err) + if moved || ask { + return c.cmdsMoved(ctx, cmds, moved, ask, addr, failedCmds) + } + return err + } + + return pipelineReadCmds(rd, cmds) + }) + }) + }) +} + +func (c *ClusterClient) txPipelineReadQueued( + ctx context.Context, + rd *proto.Reader, + statusCmd *StatusCmd, + cmds []Cmder, + failedCmds *cmdsMap, +) error { + // Parse queued replies. + if err := statusCmd.readReply(rd); err != nil { + return err + } + + for _, cmd := range cmds { + err := statusCmd.readReply(rd) + if err == nil || c.checkMovedErr(ctx, cmd, err, failedCmds) || isRedisError(err) { + continue + } + return err + } + + // Parse number of replies. + line, err := rd.ReadLine() + if err != nil { + if err == Nil { + err = TxFailedErr + } + return err + } + + switch line[0] { + case proto.ErrorReply: + return proto.ParseErrorReply(line) + case proto.ArrayReply: + // ok + default: + return fmt.Errorf("redis: expected '*', but got line %q", line) + } + + return nil +} + +func (c *ClusterClient) cmdsMoved( + ctx context.Context, cmds []Cmder, + moved, ask bool, + addr string, + failedCmds *cmdsMap, +) error { + node, err := c.nodes.Get(addr) + if err != nil { + return err + } + + if moved { + c.state.LazyReload(ctx) + for _, cmd := range cmds { + failedCmds.Add(node, cmd) + } + return nil + } + + if ask { + for _, cmd := range cmds { + failedCmds.Add(node, NewCmd(ctx, "asking"), cmd) + } + return nil + } + + return nil +} + +func (c *ClusterClient) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error { + if len(keys) == 0 { + return fmt.Errorf("redis: Watch requires at least one key") + } + + slot := hashtag.Slot(keys[0]) + for _, key := range keys[1:] { + if hashtag.Slot(key) != slot { + err := fmt.Errorf("redis: Watch requires all keys to be in the same slot") + return err + } + } + + node, err := c.slotMasterNode(ctx, slot) + if err != nil { + return err + } + + for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ { + if attempt > 0 { + if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { + return err + } + } + + err = node.Client.Watch(ctx, fn, keys...) + if err == nil { + break + } + + moved, ask, addr := isMovedError(err) + if moved || ask { + node, err = c.nodes.Get(addr) + if err != nil { + return err + } + continue + } + + if isReadOnly := isReadOnlyError(err); isReadOnly || err == pool.ErrClosed { + if isReadOnly { + c.state.LazyReload(ctx) + } + node, err = c.slotMasterNode(ctx, slot) + if err != nil { + return err + } + continue + } + + if shouldRetry(err, true) { + continue + } + + return err + } + + return err +} + +func (c *ClusterClient) pubSub() *PubSub { + var node *clusterNode + pubsub := &PubSub{ + opt: c.opt.clientOptions(), + + newConn: func(ctx context.Context, channels []string) (*pool.Conn, error) { + if node != nil { + panic("node != nil") + } + + var err error + if len(channels) > 0 { + slot := hashtag.Slot(channels[0]) + node, err = c.slotMasterNode(ctx, slot) + } else { + node, err = c.nodes.Random() + } + if err != nil { + return nil, err + } + + cn, err := node.Client.newConn(context.TODO()) + if err != nil { + node = nil + + return nil, err + } + + return cn, nil + }, + closeConn: func(cn *pool.Conn) error { + err := node.Client.connPool.CloseConn(cn) + node = nil + return err + }, + } + pubsub.init() + + return pubsub +} + +// Subscribe subscribes the client to the specified channels. +// Channels can be omitted to create empty subscription. +func (c *ClusterClient) Subscribe(ctx context.Context, channels ...string) *PubSub { + pubsub := c.pubSub() + if len(channels) > 0 { + _ = pubsub.Subscribe(ctx, channels...) + } + return pubsub +} + +// PSubscribe subscribes the client to the given patterns. +// Patterns can be omitted to create empty subscription. +func (c *ClusterClient) PSubscribe(ctx context.Context, channels ...string) *PubSub { + pubsub := c.pubSub() + if len(channels) > 0 { + _ = pubsub.PSubscribe(ctx, channels...) + } + return pubsub +} + +func (c *ClusterClient) retryBackoff(attempt int) time.Duration { + return internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff) +} + +func (c *ClusterClient) cmdsInfo() (map[string]*CommandInfo, error) { + // Try 3 random nodes. + const nodeLimit = 3 + + addrs, err := c.nodes.Addrs() + if err != nil { + return nil, err + } + + var firstErr error + + perm := rand.Perm(len(addrs)) + if len(perm) > nodeLimit { + perm = perm[:nodeLimit] + } + + for _, idx := range perm { + addr := addrs[idx] + + node, err := c.nodes.Get(addr) + if err != nil { + if firstErr == nil { + firstErr = err + } + continue + } + + info, err := node.Client.Command(c.ctx).Result() + if err == nil { + return info, nil + } + if firstErr == nil { + firstErr = err + } + } + + if firstErr == nil { + panic("not reached") + } + return nil, firstErr +} + +func (c *ClusterClient) cmdInfo(name string) *CommandInfo { + cmdsInfo, err := c.cmdsInfoCache.Get() + if err != nil { + return nil + } + + info := cmdsInfo[name] + if info == nil { + internal.Logger.Printf(c.Context(), "info for cmd=%s not found", name) + } + return info +} + +func (c *ClusterClient) cmdSlot(cmd Cmder) int { + args := cmd.Args() + if args[0] == "cluster" && args[1] == "getkeysinslot" { + return args[2].(int) + } + + cmdInfo := c.cmdInfo(cmd.Name()) + return cmdSlot(cmd, cmdFirstKeyPos(cmd, cmdInfo)) +} + +func cmdSlot(cmd Cmder, pos int) int { + if pos == 0 { + return hashtag.RandomSlot() + } + firstKey := cmd.stringArg(pos) + return hashtag.Slot(firstKey) +} + +func (c *ClusterClient) cmdNode( + ctx context.Context, + cmdInfo *CommandInfo, + slot int, +) (*clusterNode, error) { + state, err := c.state.Get(ctx) + if err != nil { + return nil, err + } + + if (c.opt.RouteByLatency || c.opt.RouteRandomly) && cmdInfo != nil && cmdInfo.ReadOnly { + return c.slotReadOnlyNode(state, slot) + } + return state.slotMasterNode(slot) +} + +func (c *clusterClient) slotReadOnlyNode(state *clusterState, slot int) (*clusterNode, error) { + if c.opt.RouteByLatency { + return state.slotClosestNode(slot) + } + if c.opt.RouteRandomly { + return state.slotRandomNode(slot) + } + return state.slotSlaveNode(slot) +} + +func (c *ClusterClient) slotMasterNode(ctx context.Context, slot int) (*clusterNode, error) { + state, err := c.state.Get(ctx) + if err != nil { + return nil, err + } + return state.slotMasterNode(slot) +} + +func appendUniqueNode(nodes []*clusterNode, node *clusterNode) []*clusterNode { + for _, n := range nodes { + if n == node { + return nodes + } + } + return append(nodes, node) +} + +func appendIfNotExists(ss []string, es ...string) []string { +loop: + for _, e := range es { + for _, s := range ss { + if s == e { + continue loop + } + } + ss = append(ss, e) + } + return ss +} + +//------------------------------------------------------------------------------ + +type cmdsMap struct { + mu sync.Mutex + m map[*clusterNode][]Cmder +} + +func newCmdsMap() *cmdsMap { + return &cmdsMap{ + m: make(map[*clusterNode][]Cmder), + } +} + +func (m *cmdsMap) Add(node *clusterNode, cmds ...Cmder) { + m.mu.Lock() + m.m[node] = append(m.m[node], cmds...) + m.mu.Unlock() +} diff --git a/vendor/github.com/go-redis/redis/v8/cluster_commands.go b/vendor/github.com/go-redis/redis/v8/cluster_commands.go new file mode 100644 index 0000000000..1f0bae067a --- /dev/null +++ b/vendor/github.com/go-redis/redis/v8/cluster_commands.go @@ -0,0 +1,25 @@ +package redis + +import ( + "context" + "sync/atomic" +) + +func (c *ClusterClient) DBSize(ctx context.Context) *IntCmd { + cmd := NewIntCmd(ctx, "dbsize") + var size int64 + err := c.ForEachMaster(ctx, func(ctx context.Context, master *Client) error { + n, err := master.DBSize(ctx).Result() + if err != nil { + return err + } + atomic.AddInt64(&size, n) + return nil + }) + if err != nil { + cmd.SetErr(err) + return cmd + } + cmd.val = size + return cmd +} diff --git a/vendor/github.com/go-redis/redis/v8/command.go b/vendor/github.com/go-redis/redis/v8/command.go new file mode 100644 index 0000000000..1cf761682a --- /dev/null +++ b/vendor/github.com/go-redis/redis/v8/command.go @@ -0,0 +1,2285 @@ +package redis + +import ( + "context" + "fmt" + "net" + "strconv" + "time" + + "github.com/go-redis/redis/v8/internal" + "github.com/go-redis/redis/v8/internal/proto" + "github.com/go-redis/redis/v8/internal/util" +) + +type Cmder interface { + Name() string + FullName() string + Args() []interface{} + String() string + stringArg(int) string + + readTimeout() *time.Duration + readReply(rd *proto.Reader) error + + SetErr(error) + Err() error +} + +func setCmdsErr(cmds []Cmder, e error) { + for _, cmd := range cmds { + if cmd.Err() == nil { + cmd.SetErr(e) + } + } +} + +func cmdsFirstErr(cmds []Cmder) error { + for _, cmd := range cmds { + if err := cmd.Err(); err != nil { + return err + } + } + return nil +} + +func writeCmds(wr *proto.Writer, cmds []Cmder) error { + for _, cmd := range cmds { + if err := writeCmd(wr, cmd); err != nil { + return err + } + } + return nil +} + +func writeCmd(wr *proto.Writer, cmd Cmder) error { + return wr.WriteArgs(cmd.Args()) +} + +func cmdFirstKeyPos(cmd Cmder, info *CommandInfo) int { + switch cmd.Name() { + case "eval", "evalsha": + if cmd.stringArg(2) != "0" { + return 3 + } + + return 0 + case "publish": + return 1 + case "memory": + // https://github.com/redis/redis/issues/7493 + if cmd.stringArg(1) == "usage" { + return 2 + } + } + + if info == nil { + return 0 + } + return int(info.FirstKeyPos) +} + +func cmdString(cmd Cmder, val interface{}) string { + b := make([]byte, 0, 64) + + for i, arg := range cmd.Args() { + if i > 0 { + b = append(b, ' ') + } + b = internal.AppendArg(b, arg) + } + + if err := cmd.Err(); err != nil { + b = append(b, ": "...) + b = append(b, err.Error()...) + } else if val != nil { + b = append(b, ": "...) + b = internal.AppendArg(b, val) + } + + return internal.String(b) +} + +//------------------------------------------------------------------------------ + +type baseCmd struct { + ctx context.Context + args []interface{} + err error + + _readTimeout *time.Duration +} + +var _ Cmder = (*Cmd)(nil) + +func (cmd *baseCmd) Name() string { + if len(cmd.args) == 0 { + return "" + } + // Cmd name must be lower cased. + return internal.ToLower(cmd.stringArg(0)) +} + +func (cmd *baseCmd) FullName() string { + switch name := cmd.Name(); name { + case "cluster", "command": + if len(cmd.args) == 1 { + return name + } + if s2, ok := cmd.args[1].(string); ok { + return name + " " + s2 + } + return name + default: + return name + } +} + +func (cmd *baseCmd) Args() []interface{} { + return cmd.args +} + +func (cmd *baseCmd) stringArg(pos int) string { + if pos < 0 || pos >= len(cmd.args) { + return "" + } + s, _ := cmd.args[pos].(string) + return s +} + +func (cmd *baseCmd) SetErr(e error) { + cmd.err = e +} + +func (cmd *baseCmd) Err() error { + return cmd.err +} + +func (cmd *baseCmd) readTimeout() *time.Duration { + return cmd._readTimeout +} + +func (cmd *baseCmd) setReadTimeout(d time.Duration) { + cmd._readTimeout = &d +} + +//------------------------------------------------------------------------------ + +type Cmd struct { + baseCmd + + val interface{} +} + +func NewCmd(ctx context.Context, args ...interface{}) *Cmd { + return &Cmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *Cmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *Cmd) Val() interface{} { + return cmd.val +} + +func (cmd *Cmd) Result() (interface{}, error) { + return cmd.val, cmd.err +} + +func (cmd *Cmd) Text() (string, error) { + if cmd.err != nil { + return "", cmd.err + } + switch val := cmd.val.(type) { + case string: + return val, nil + default: + err := fmt.Errorf("redis: unexpected type=%T for String", val) + return "", err + } +} + +func (cmd *Cmd) Int() (int, error) { + if cmd.err != nil { + return 0, cmd.err + } + switch val := cmd.val.(type) { + case int64: + return int(val), nil + case string: + return strconv.Atoi(val) + default: + err := fmt.Errorf("redis: unexpected type=%T for Int", val) + return 0, err + } +} + +func (cmd *Cmd) Int64() (int64, error) { + if cmd.err != nil { + return 0, cmd.err + } + switch val := cmd.val.(type) { + case int64: + return val, nil + case string: + return strconv.ParseInt(val, 10, 64) + default: + err := fmt.Errorf("redis: unexpected type=%T for Int64", val) + return 0, err + } +} + +func (cmd *Cmd) Uint64() (uint64, error) { + if cmd.err != nil { + return 0, cmd.err + } + switch val := cmd.val.(type) { + case int64: + return uint64(val), nil + case string: + return strconv.ParseUint(val, 10, 64) + default: + err := fmt.Errorf("redis: unexpected type=%T for Uint64", val) + return 0, err + } +} + +func (cmd *Cmd) Float32() (float32, error) { + if cmd.err != nil { + return 0, cmd.err + } + switch val := cmd.val.(type) { + case int64: + return float32(val), nil + case string: + f, err := strconv.ParseFloat(val, 32) + if err != nil { + return 0, err + } + return float32(f), nil + default: + err := fmt.Errorf("redis: unexpected type=%T for Float32", val) + return 0, err + } +} + +func (cmd *Cmd) Float64() (float64, error) { + if cmd.err != nil { + return 0, cmd.err + } + switch val := cmd.val.(type) { + case int64: + return float64(val), nil + case string: + return strconv.ParseFloat(val, 64) + default: + err := fmt.Errorf("redis: unexpected type=%T for Float64", val) + return 0, err + } +} + +func (cmd *Cmd) Bool() (bool, error) { + if cmd.err != nil { + return false, cmd.err + } + switch val := cmd.val.(type) { + case int64: + return val != 0, nil + case string: + return strconv.ParseBool(val) + default: + err := fmt.Errorf("redis: unexpected type=%T for Bool", val) + return false, err + } +} + +func (cmd *Cmd) readReply(rd *proto.Reader) error { + cmd.val, cmd.err = rd.ReadReply(sliceParser) + return cmd.err +} + +// sliceParser implements proto.MultiBulkParse. +func sliceParser(rd *proto.Reader, n int64) (interface{}, error) { + vals := make([]interface{}, n) + for i := 0; i < len(vals); i++ { + v, err := rd.ReadReply(sliceParser) + if err != nil { + if err == Nil { + vals[i] = nil + continue + } + if err, ok := err.(proto.RedisError); ok { + vals[i] = err + continue + } + return nil, err + } + vals[i] = v + } + return vals, nil +} + +//------------------------------------------------------------------------------ + +type SliceCmd struct { + baseCmd + + val []interface{} +} + +var _ Cmder = (*SliceCmd)(nil) + +func NewSliceCmd(ctx context.Context, args ...interface{}) *SliceCmd { + return &SliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *SliceCmd) Val() []interface{} { + return cmd.val +} + +func (cmd *SliceCmd) Result() ([]interface{}, error) { + return cmd.val, cmd.err +} + +func (cmd *SliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *SliceCmd) readReply(rd *proto.Reader) error { + var v interface{} + v, cmd.err = rd.ReadArrayReply(sliceParser) + if cmd.err != nil { + return cmd.err + } + cmd.val = v.([]interface{}) + return nil +} + +//------------------------------------------------------------------------------ + +type StatusCmd struct { + baseCmd + + val string +} + +var _ Cmder = (*StatusCmd)(nil) + +func NewStatusCmd(ctx context.Context, args ...interface{}) *StatusCmd { + return &StatusCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *StatusCmd) Val() string { + return cmd.val +} + +func (cmd *StatusCmd) Result() (string, error) { + return cmd.val, cmd.err +} + +func (cmd *StatusCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *StatusCmd) readReply(rd *proto.Reader) error { + cmd.val, cmd.err = rd.ReadString() + return cmd.err +} + +//------------------------------------------------------------------------------ + +type IntCmd struct { + baseCmd + + val int64 +} + +var _ Cmder = (*IntCmd)(nil) + +func NewIntCmd(ctx context.Context, args ...interface{}) *IntCmd { + return &IntCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *IntCmd) Val() int64 { + return cmd.val +} + +func (cmd *IntCmd) Result() (int64, error) { + return cmd.val, cmd.err +} + +func (cmd *IntCmd) Uint64() (uint64, error) { + return uint64(cmd.val), cmd.err +} + +func (cmd *IntCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *IntCmd) readReply(rd *proto.Reader) error { + cmd.val, cmd.err = rd.ReadIntReply() + return cmd.err +} + +//------------------------------------------------------------------------------ + +type IntSliceCmd struct { + baseCmd + + val []int64 +} + +var _ Cmder = (*IntSliceCmd)(nil) + +func NewIntSliceCmd(ctx context.Context, args ...interface{}) *IntSliceCmd { + return &IntSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *IntSliceCmd) Val() []int64 { + return cmd.val +} + +func (cmd *IntSliceCmd) Result() ([]int64, error) { + return cmd.val, cmd.err +} + +func (cmd *IntSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *IntSliceCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]int64, n) + for i := 0; i < len(cmd.val); i++ { + num, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + cmd.val[i] = num + } + return nil, nil + }) + return cmd.err +} + +//------------------------------------------------------------------------------ + +type DurationCmd struct { + baseCmd + + val time.Duration + precision time.Duration +} + +var _ Cmder = (*DurationCmd)(nil) + +func NewDurationCmd(ctx context.Context, precision time.Duration, args ...interface{}) *DurationCmd { + return &DurationCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + precision: precision, + } +} + +func (cmd *DurationCmd) Val() time.Duration { + return cmd.val +} + +func (cmd *DurationCmd) Result() (time.Duration, error) { + return cmd.val, cmd.err +} + +func (cmd *DurationCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *DurationCmd) readReply(rd *proto.Reader) error { + var n int64 + n, cmd.err = rd.ReadIntReply() + if cmd.err != nil { + return cmd.err + } + switch n { + // -2 if the key does not exist + // -1 if the key exists but has no associated expire + case -2, -1: + cmd.val = time.Duration(n) + default: + cmd.val = time.Duration(n) * cmd.precision + } + return nil +} + +//------------------------------------------------------------------------------ + +type TimeCmd struct { + baseCmd + + val time.Time +} + +var _ Cmder = (*TimeCmd)(nil) + +func NewTimeCmd(ctx context.Context, args ...interface{}) *TimeCmd { + return &TimeCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *TimeCmd) Val() time.Time { + return cmd.val +} + +func (cmd *TimeCmd) Result() (time.Time, error) { + return cmd.val, cmd.err +} + +func (cmd *TimeCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *TimeCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + if n != 2 { + return nil, fmt.Errorf("got %d elements, expected 2", n) + } + + sec, err := rd.ReadInt() + if err != nil { + return nil, err + } + + microsec, err := rd.ReadInt() + if err != nil { + return nil, err + } + + cmd.val = time.Unix(sec, microsec*1000) + return nil, nil + }) + return cmd.err +} + +//------------------------------------------------------------------------------ + +type BoolCmd struct { + baseCmd + + val bool +} + +var _ Cmder = (*BoolCmd)(nil) + +func NewBoolCmd(ctx context.Context, args ...interface{}) *BoolCmd { + return &BoolCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *BoolCmd) Val() bool { + return cmd.val +} + +func (cmd *BoolCmd) Result() (bool, error) { + return cmd.val, cmd.err +} + +func (cmd *BoolCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *BoolCmd) readReply(rd *proto.Reader) error { + var v interface{} + v, cmd.err = rd.ReadReply(nil) + // `SET key value NX` returns nil when key already exists. But + // `SETNX key value` returns bool (0/1). So convert nil to bool. + if cmd.err == Nil { + cmd.val = false + cmd.err = nil + return nil + } + if cmd.err != nil { + return cmd.err + } + switch v := v.(type) { + case int64: + cmd.val = v == 1 + return nil + case string: + cmd.val = v == "OK" + return nil + default: + cmd.err = fmt.Errorf("got %T, wanted int64 or string", v) + return cmd.err + } +} + +//------------------------------------------------------------------------------ + +type StringCmd struct { + baseCmd + + val string +} + +var _ Cmder = (*StringCmd)(nil) + +func NewStringCmd(ctx context.Context, args ...interface{}) *StringCmd { + return &StringCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *StringCmd) Val() string { + return cmd.val +} + +func (cmd *StringCmd) Result() (string, error) { + return cmd.Val(), cmd.err +} + +func (cmd *StringCmd) Bytes() ([]byte, error) { + return util.StringToBytes(cmd.val), cmd.err +} + +func (cmd *StringCmd) Int() (int, error) { + if cmd.err != nil { + return 0, cmd.err + } + return strconv.Atoi(cmd.Val()) +} + +func (cmd *StringCmd) Int64() (int64, error) { + if cmd.err != nil { + return 0, cmd.err + } + return strconv.ParseInt(cmd.Val(), 10, 64) +} + +func (cmd *StringCmd) Uint64() (uint64, error) { + if cmd.err != nil { + return 0, cmd.err + } + return strconv.ParseUint(cmd.Val(), 10, 64) +} + +func (cmd *StringCmd) Float32() (float32, error) { + if cmd.err != nil { + return 0, cmd.err + } + f, err := strconv.ParseFloat(cmd.Val(), 32) + if err != nil { + return 0, err + } + return float32(f), nil +} + +func (cmd *StringCmd) Float64() (float64, error) { + if cmd.err != nil { + return 0, cmd.err + } + return strconv.ParseFloat(cmd.Val(), 64) +} + +func (cmd *StringCmd) Time() (time.Time, error) { + if cmd.err != nil { + return time.Time{}, cmd.err + } + return time.Parse(time.RFC3339Nano, cmd.Val()) +} + +func (cmd *StringCmd) Scan(val interface{}) error { + if cmd.err != nil { + return cmd.err + } + return proto.Scan([]byte(cmd.val), val) +} + +func (cmd *StringCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *StringCmd) readReply(rd *proto.Reader) error { + cmd.val, cmd.err = rd.ReadString() + return cmd.err +} + +//------------------------------------------------------------------------------ + +type FloatCmd struct { + baseCmd + + val float64 +} + +var _ Cmder = (*FloatCmd)(nil) + +func NewFloatCmd(ctx context.Context, args ...interface{}) *FloatCmd { + return &FloatCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *FloatCmd) Val() float64 { + return cmd.val +} + +func (cmd *FloatCmd) Result() (float64, error) { + return cmd.Val(), cmd.Err() +} + +func (cmd *FloatCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *FloatCmd) readReply(rd *proto.Reader) error { + cmd.val, cmd.err = rd.ReadFloatReply() + return cmd.err +} + +//------------------------------------------------------------------------------ + +type StringSliceCmd struct { + baseCmd + + val []string +} + +var _ Cmder = (*StringSliceCmd)(nil) + +func NewStringSliceCmd(ctx context.Context, args ...interface{}) *StringSliceCmd { + return &StringSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *StringSliceCmd) Val() []string { + return cmd.val +} + +func (cmd *StringSliceCmd) Result() ([]string, error) { + return cmd.Val(), cmd.Err() +} + +func (cmd *StringSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *StringSliceCmd) ScanSlice(container interface{}) error { + return proto.ScanSlice(cmd.Val(), container) +} + +func (cmd *StringSliceCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]string, n) + for i := 0; i < len(cmd.val); i++ { + switch s, err := rd.ReadString(); { + case err == Nil: + cmd.val[i] = "" + case err != nil: + return nil, err + default: + cmd.val[i] = s + } + } + return nil, nil + }) + return cmd.err +} + +//------------------------------------------------------------------------------ + +type BoolSliceCmd struct { + baseCmd + + val []bool +} + +var _ Cmder = (*BoolSliceCmd)(nil) + +func NewBoolSliceCmd(ctx context.Context, args ...interface{}) *BoolSliceCmd { + return &BoolSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *BoolSliceCmd) Val() []bool { + return cmd.val +} + +func (cmd *BoolSliceCmd) Result() ([]bool, error) { + return cmd.val, cmd.err +} + +func (cmd *BoolSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *BoolSliceCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]bool, n) + for i := 0; i < len(cmd.val); i++ { + n, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + cmd.val[i] = n == 1 + } + return nil, nil + }) + return cmd.err +} + +//------------------------------------------------------------------------------ + +type StringStringMapCmd struct { + baseCmd + + val map[string]string +} + +var _ Cmder = (*StringStringMapCmd)(nil) + +func NewStringStringMapCmd(ctx context.Context, args ...interface{}) *StringStringMapCmd { + return &StringStringMapCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *StringStringMapCmd) Val() map[string]string { + return cmd.val +} + +func (cmd *StringStringMapCmd) Result() (map[string]string, error) { + return cmd.val, cmd.err +} + +func (cmd *StringStringMapCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *StringStringMapCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make(map[string]string, n/2) + for i := int64(0); i < n; i += 2 { + key, err := rd.ReadString() + if err != nil { + return nil, err + } + + value, err := rd.ReadString() + if err != nil { + return nil, err + } + + cmd.val[key] = value + } + return nil, nil + }) + return cmd.err +} + +//------------------------------------------------------------------------------ + +type StringIntMapCmd struct { + baseCmd + + val map[string]int64 +} + +var _ Cmder = (*StringIntMapCmd)(nil) + +func NewStringIntMapCmd(ctx context.Context, args ...interface{}) *StringIntMapCmd { + return &StringIntMapCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *StringIntMapCmd) Val() map[string]int64 { + return cmd.val +} + +func (cmd *StringIntMapCmd) Result() (map[string]int64, error) { + return cmd.val, cmd.err +} + +func (cmd *StringIntMapCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *StringIntMapCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make(map[string]int64, n/2) + for i := int64(0); i < n; i += 2 { + key, err := rd.ReadString() + if err != nil { + return nil, err + } + + n, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + + cmd.val[key] = n + } + return nil, nil + }) + return cmd.err +} + +//------------------------------------------------------------------------------ + +type StringStructMapCmd struct { + baseCmd + + val map[string]struct{} +} + +var _ Cmder = (*StringStructMapCmd)(nil) + +func NewStringStructMapCmd(ctx context.Context, args ...interface{}) *StringStructMapCmd { + return &StringStructMapCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *StringStructMapCmd) Val() map[string]struct{} { + return cmd.val +} + +func (cmd *StringStructMapCmd) Result() (map[string]struct{}, error) { + return cmd.val, cmd.err +} + +func (cmd *StringStructMapCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *StringStructMapCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make(map[string]struct{}, n) + for i := int64(0); i < n; i++ { + key, err := rd.ReadString() + if err != nil { + return nil, err + } + cmd.val[key] = struct{}{} + } + return nil, nil + }) + return cmd.err +} + +//------------------------------------------------------------------------------ + +type XMessage struct { + ID string + Values map[string]interface{} +} + +type XMessageSliceCmd struct { + baseCmd + + val []XMessage +} + +var _ Cmder = (*XMessageSliceCmd)(nil) + +func NewXMessageSliceCmd(ctx context.Context, args ...interface{}) *XMessageSliceCmd { + return &XMessageSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *XMessageSliceCmd) Val() []XMessage { + return cmd.val +} + +func (cmd *XMessageSliceCmd) Result() ([]XMessage, error) { + return cmd.val, cmd.err +} + +func (cmd *XMessageSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *XMessageSliceCmd) readReply(rd *proto.Reader) error { + var v interface{} + v, cmd.err = rd.ReadArrayReply(xMessageSliceParser) + if cmd.err != nil { + return cmd.err + } + cmd.val = v.([]XMessage) + return nil +} + +// xMessageSliceParser implements proto.MultiBulkParse. +func xMessageSliceParser(rd *proto.Reader, n int64) (interface{}, error) { + msgs := make([]XMessage, n) + for i := 0; i < len(msgs); i++ { + i := i + _, err := rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + id, err := rd.ReadString() + if err != nil { + return nil, err + } + + var values map[string]interface{} + + v, err := rd.ReadArrayReply(stringInterfaceMapParser) + if err != nil { + if err != proto.Nil { + return nil, err + } + } else { + values = v.(map[string]interface{}) + } + + msgs[i] = XMessage{ + ID: id, + Values: values, + } + return nil, nil + }) + if err != nil { + return nil, err + } + } + return msgs, nil +} + +// stringInterfaceMapParser implements proto.MultiBulkParse. +func stringInterfaceMapParser(rd *proto.Reader, n int64) (interface{}, error) { + m := make(map[string]interface{}, n/2) + for i := int64(0); i < n; i += 2 { + key, err := rd.ReadString() + if err != nil { + return nil, err + } + + value, err := rd.ReadString() + if err != nil { + return nil, err + } + + m[key] = value + } + return m, nil +} + +//------------------------------------------------------------------------------ + +type XStream struct { + Stream string + Messages []XMessage +} + +type XStreamSliceCmd struct { + baseCmd + + val []XStream +} + +var _ Cmder = (*XStreamSliceCmd)(nil) + +func NewXStreamSliceCmd(ctx context.Context, args ...interface{}) *XStreamSliceCmd { + return &XStreamSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *XStreamSliceCmd) Val() []XStream { + return cmd.val +} + +func (cmd *XStreamSliceCmd) Result() ([]XStream, error) { + return cmd.val, cmd.err +} + +func (cmd *XStreamSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *XStreamSliceCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]XStream, n) + for i := 0; i < len(cmd.val); i++ { + i := i + _, err := rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + if n != 2 { + return nil, fmt.Errorf("got %d, wanted 2", n) + } + + stream, err := rd.ReadString() + if err != nil { + return nil, err + } + + v, err := rd.ReadArrayReply(xMessageSliceParser) + if err != nil { + return nil, err + } + + cmd.val[i] = XStream{ + Stream: stream, + Messages: v.([]XMessage), + } + return nil, nil + }) + if err != nil { + return nil, err + } + } + return nil, nil + }) + return cmd.err +} + +//------------------------------------------------------------------------------ + +type XPending struct { + Count int64 + Lower string + Higher string + Consumers map[string]int64 +} + +type XPendingCmd struct { + baseCmd + val *XPending +} + +var _ Cmder = (*XPendingCmd)(nil) + +func NewXPendingCmd(ctx context.Context, args ...interface{}) *XPendingCmd { + return &XPendingCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *XPendingCmd) Val() *XPending { + return cmd.val +} + +func (cmd *XPendingCmd) Result() (*XPending, error) { + return cmd.val, cmd.err +} + +func (cmd *XPendingCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *XPendingCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + if n != 4 { + return nil, fmt.Errorf("got %d, wanted 4", n) + } + + count, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + + lower, err := rd.ReadString() + if err != nil && err != Nil { + return nil, err + } + + higher, err := rd.ReadString() + if err != nil && err != Nil { + return nil, err + } + + cmd.val = &XPending{ + Count: count, + Lower: lower, + Higher: higher, + } + _, err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + for i := int64(0); i < n; i++ { + _, err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + if n != 2 { + return nil, fmt.Errorf("got %d, wanted 2", n) + } + + consumerName, err := rd.ReadString() + if err != nil { + return nil, err + } + + consumerPending, err := rd.ReadInt() + if err != nil { + return nil, err + } + + if cmd.val.Consumers == nil { + cmd.val.Consumers = make(map[string]int64) + } + cmd.val.Consumers[consumerName] = consumerPending + + return nil, nil + }) + if err != nil { + return nil, err + } + } + return nil, nil + }) + if err != nil && err != Nil { + return nil, err + } + + return nil, nil + }) + return cmd.err +} + +//------------------------------------------------------------------------------ + +type XPendingExt struct { + ID string + Consumer string + Idle time.Duration + RetryCount int64 +} + +type XPendingExtCmd struct { + baseCmd + val []XPendingExt +} + +var _ Cmder = (*XPendingExtCmd)(nil) + +func NewXPendingExtCmd(ctx context.Context, args ...interface{}) *XPendingExtCmd { + return &XPendingExtCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *XPendingExtCmd) Val() []XPendingExt { + return cmd.val +} + +func (cmd *XPendingExtCmd) Result() ([]XPendingExt, error) { + return cmd.val, cmd.err +} + +func (cmd *XPendingExtCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *XPendingExtCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]XPendingExt, 0, n) + for i := int64(0); i < n; i++ { + _, err := rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + if n != 4 { + return nil, fmt.Errorf("got %d, wanted 4", n) + } + + id, err := rd.ReadString() + if err != nil { + return nil, err + } + + consumer, err := rd.ReadString() + if err != nil && err != Nil { + return nil, err + } + + idle, err := rd.ReadIntReply() + if err != nil && err != Nil { + return nil, err + } + + retryCount, err := rd.ReadIntReply() + if err != nil && err != Nil { + return nil, err + } + + cmd.val = append(cmd.val, XPendingExt{ + ID: id, + Consumer: consumer, + Idle: time.Duration(idle) * time.Millisecond, + RetryCount: retryCount, + }) + return nil, nil + }) + if err != nil { + return nil, err + } + } + return nil, nil + }) + return cmd.err +} + +//------------------------------------------------------------------------------ + +type XInfoGroupsCmd struct { + baseCmd + val []XInfoGroups +} + +type XInfoGroups struct { + Name string + Consumers int64 + Pending int64 + LastDeliveredID string +} + +var _ Cmder = (*XInfoGroupsCmd)(nil) + +func NewXInfoGroupsCmd(ctx context.Context, stream string) *XInfoGroupsCmd { + return &XInfoGroupsCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: []interface{}{"xinfo", "groups", stream}, + }, + } +} + +func (cmd *XInfoGroupsCmd) Val() []XInfoGroups { + return cmd.val +} + +func (cmd *XInfoGroupsCmd) Result() ([]XInfoGroups, error) { + return cmd.val, cmd.err +} + +func (cmd *XInfoGroupsCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *XInfoGroupsCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply( + func(rd *proto.Reader, n int64) (interface{}, error) { + for i := int64(0); i < n; i++ { + v, err := rd.ReadReply(xGroupInfoParser) + if err != nil { + return nil, err + } + cmd.val = append(cmd.val, v.(XInfoGroups)) + } + return nil, nil + }) + return nil +} + +func xGroupInfoParser(rd *proto.Reader, n int64) (interface{}, error) { + if n != 8 { + return nil, fmt.Errorf("redis: got %d elements in XINFO GROUPS reply,"+ + "wanted 8", n) + } + var ( + err error + grp XInfoGroups + key string + val string + ) + + for i := 0; i < 4; i++ { + key, err = rd.ReadString() + if err != nil { + return nil, err + } + val, err = rd.ReadString() + if err != nil { + return nil, err + } + switch key { + case "name": + grp.Name = val + case "consumers": + grp.Consumers, err = strconv.ParseInt(val, 0, 64) + case "pending": + grp.Pending, err = strconv.ParseInt(val, 0, 64) + case "last-delivered-id": + grp.LastDeliveredID = val + default: + return nil, fmt.Errorf("redis: unexpected content %s "+ + "in XINFO GROUPS reply", key) + } + if err != nil { + return nil, err + } + } + return grp, err +} + +//------------------------------------------------------------------------------ + +type ZSliceCmd struct { + baseCmd + + val []Z +} + +var _ Cmder = (*ZSliceCmd)(nil) + +func NewZSliceCmd(ctx context.Context, args ...interface{}) *ZSliceCmd { + return &ZSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *ZSliceCmd) Val() []Z { + return cmd.val +} + +func (cmd *ZSliceCmd) Result() ([]Z, error) { + return cmd.val, cmd.err +} + +func (cmd *ZSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *ZSliceCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]Z, n/2) + for i := 0; i < len(cmd.val); i++ { + member, err := rd.ReadString() + if err != nil { + return nil, err + } + + score, err := rd.ReadFloatReply() + if err != nil { + return nil, err + } + + cmd.val[i] = Z{ + Member: member, + Score: score, + } + } + return nil, nil + }) + return cmd.err +} + +//------------------------------------------------------------------------------ + +type ZWithKeyCmd struct { + baseCmd + + val *ZWithKey +} + +var _ Cmder = (*ZWithKeyCmd)(nil) + +func NewZWithKeyCmd(ctx context.Context, args ...interface{}) *ZWithKeyCmd { + return &ZWithKeyCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *ZWithKeyCmd) Val() *ZWithKey { + return cmd.val +} + +func (cmd *ZWithKeyCmd) Result() (*ZWithKey, error) { + return cmd.Val(), cmd.Err() +} + +func (cmd *ZWithKeyCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *ZWithKeyCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + if n != 3 { + return nil, fmt.Errorf("got %d elements, expected 3", n) + } + + cmd.val = &ZWithKey{} + var err error + + cmd.val.Key, err = rd.ReadString() + if err != nil { + return nil, err + } + + cmd.val.Member, err = rd.ReadString() + if err != nil { + return nil, err + } + + cmd.val.Score, err = rd.ReadFloatReply() + if err != nil { + return nil, err + } + + return nil, nil + }) + return cmd.err +} + +//------------------------------------------------------------------------------ + +type ScanCmd struct { + baseCmd + + page []string + cursor uint64 + + process cmdable +} + +var _ Cmder = (*ScanCmd)(nil) + +func NewScanCmd(ctx context.Context, process cmdable, args ...interface{}) *ScanCmd { + return &ScanCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + process: process, + } +} + +func (cmd *ScanCmd) Val() (keys []string, cursor uint64) { + return cmd.page, cmd.cursor +} + +func (cmd *ScanCmd) Result() (keys []string, cursor uint64, err error) { + return cmd.page, cmd.cursor, cmd.err +} + +func (cmd *ScanCmd) String() string { + return cmdString(cmd, cmd.page) +} + +func (cmd *ScanCmd) readReply(rd *proto.Reader) error { + cmd.page, cmd.cursor, cmd.err = rd.ReadScanReply() + return cmd.err +} + +// Iterator creates a new ScanIterator. +func (cmd *ScanCmd) Iterator() *ScanIterator { + return &ScanIterator{ + cmd: cmd, + } +} + +//------------------------------------------------------------------------------ + +type ClusterNode struct { + ID string + Addr string +} + +type ClusterSlot struct { + Start int + End int + Nodes []ClusterNode +} + +type ClusterSlotsCmd struct { + baseCmd + + val []ClusterSlot +} + +var _ Cmder = (*ClusterSlotsCmd)(nil) + +func NewClusterSlotsCmd(ctx context.Context, args ...interface{}) *ClusterSlotsCmd { + return &ClusterSlotsCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *ClusterSlotsCmd) Val() []ClusterSlot { + return cmd.val +} + +func (cmd *ClusterSlotsCmd) Result() ([]ClusterSlot, error) { + return cmd.Val(), cmd.Err() +} + +func (cmd *ClusterSlotsCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *ClusterSlotsCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]ClusterSlot, n) + for i := 0; i < len(cmd.val); i++ { + n, err := rd.ReadArrayLen() + if err != nil { + return nil, err + } + if n < 2 { + err := fmt.Errorf("redis: got %d elements in cluster info, expected at least 2", n) + return nil, err + } + + start, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + + end, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + + nodes := make([]ClusterNode, n-2) + for j := 0; j < len(nodes); j++ { + n, err := rd.ReadArrayLen() + if err != nil { + return nil, err + } + if n != 2 && n != 3 { + err := fmt.Errorf("got %d elements in cluster info address, expected 2 or 3", n) + return nil, err + } + + ip, err := rd.ReadString() + if err != nil { + return nil, err + } + + port, err := rd.ReadString() + if err != nil { + return nil, err + } + + nodes[j].Addr = net.JoinHostPort(ip, port) + + if n == 3 { + id, err := rd.ReadString() + if err != nil { + return nil, err + } + nodes[j].ID = id + } + } + + cmd.val[i] = ClusterSlot{ + Start: int(start), + End: int(end), + Nodes: nodes, + } + } + return nil, nil + }) + return cmd.err +} + +//------------------------------------------------------------------------------ + +// GeoLocation is used with GeoAdd to add geospatial location. +type GeoLocation struct { + Name string + Longitude, Latitude, Dist float64 + GeoHash int64 +} + +// GeoRadiusQuery is used with GeoRadius to query geospatial index. +type GeoRadiusQuery struct { + Radius float64 + // Can be m, km, ft, or mi. Default is km. + Unit string + WithCoord bool + WithDist bool + WithGeoHash bool + Count int + // Can be ASC or DESC. Default is no sort order. + Sort string + Store string + StoreDist string +} + +type GeoLocationCmd struct { + baseCmd + + q *GeoRadiusQuery + locations []GeoLocation +} + +var _ Cmder = (*GeoLocationCmd)(nil) + +func NewGeoLocationCmd(ctx context.Context, q *GeoRadiusQuery, args ...interface{}) *GeoLocationCmd { + return &GeoLocationCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: geoLocationArgs(q, args...), + }, + q: q, + } +} + +func geoLocationArgs(q *GeoRadiusQuery, args ...interface{}) []interface{} { + args = append(args, q.Radius) + if q.Unit != "" { + args = append(args, q.Unit) + } else { + args = append(args, "km") + } + if q.WithCoord { + args = append(args, "withcoord") + } + if q.WithDist { + args = append(args, "withdist") + } + if q.WithGeoHash { + args = append(args, "withhash") + } + if q.Count > 0 { + args = append(args, "count", q.Count) + } + if q.Sort != "" { + args = append(args, q.Sort) + } + if q.Store != "" { + args = append(args, "store") + args = append(args, q.Store) + } + if q.StoreDist != "" { + args = append(args, "storedist") + args = append(args, q.StoreDist) + } + return args +} + +func (cmd *GeoLocationCmd) Val() []GeoLocation { + return cmd.locations +} + +func (cmd *GeoLocationCmd) Result() ([]GeoLocation, error) { + return cmd.locations, cmd.err +} + +func (cmd *GeoLocationCmd) String() string { + return cmdString(cmd, cmd.locations) +} + +func (cmd *GeoLocationCmd) readReply(rd *proto.Reader) error { + var v interface{} + v, cmd.err = rd.ReadArrayReply(newGeoLocationSliceParser(cmd.q)) + if cmd.err != nil { + return cmd.err + } + cmd.locations = v.([]GeoLocation) + return nil +} + +func newGeoLocationSliceParser(q *GeoRadiusQuery) proto.MultiBulkParse { + return func(rd *proto.Reader, n int64) (interface{}, error) { + locs := make([]GeoLocation, 0, n) + for i := int64(0); i < n; i++ { + v, err := rd.ReadReply(newGeoLocationParser(q)) + if err != nil { + return nil, err + } + switch vv := v.(type) { + case string: + locs = append(locs, GeoLocation{ + Name: vv, + }) + case *GeoLocation: + // TODO: avoid copying + locs = append(locs, *vv) + default: + return nil, fmt.Errorf("got %T, expected string or *GeoLocation", v) + } + } + return locs, nil + } +} + +func newGeoLocationParser(q *GeoRadiusQuery) proto.MultiBulkParse { + return func(rd *proto.Reader, n int64) (interface{}, error) { + var loc GeoLocation + var err error + + loc.Name, err = rd.ReadString() + if err != nil { + return nil, err + } + if q.WithDist { + loc.Dist, err = rd.ReadFloatReply() + if err != nil { + return nil, err + } + } + if q.WithGeoHash { + loc.GeoHash, err = rd.ReadIntReply() + if err != nil { + return nil, err + } + } + if q.WithCoord { + n, err := rd.ReadArrayLen() + if err != nil { + return nil, err + } + if n != 2 { + return nil, fmt.Errorf("got %d coordinates, expected 2", n) + } + + loc.Longitude, err = rd.ReadFloatReply() + if err != nil { + return nil, err + } + loc.Latitude, err = rd.ReadFloatReply() + if err != nil { + return nil, err + } + } + + return &loc, nil + } +} + +//------------------------------------------------------------------------------ + +type GeoPos struct { + Longitude, Latitude float64 +} + +type GeoPosCmd struct { + baseCmd + + val []*GeoPos +} + +var _ Cmder = (*GeoPosCmd)(nil) + +func NewGeoPosCmd(ctx context.Context, args ...interface{}) *GeoPosCmd { + return &GeoPosCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *GeoPosCmd) Val() []*GeoPos { + return cmd.val +} + +func (cmd *GeoPosCmd) Result() ([]*GeoPos, error) { + return cmd.Val(), cmd.Err() +} + +func (cmd *GeoPosCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *GeoPosCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]*GeoPos, n) + for i := 0; i < len(cmd.val); i++ { + i := i + _, err := rd.ReadReply(func(rd *proto.Reader, n int64) (interface{}, error) { + longitude, err := rd.ReadFloatReply() + if err != nil { + return nil, err + } + + latitude, err := rd.ReadFloatReply() + if err != nil { + return nil, err + } + + cmd.val[i] = &GeoPos{ + Longitude: longitude, + Latitude: latitude, + } + return nil, nil + }) + if err != nil { + if err == Nil { + cmd.val[i] = nil + continue + } + return nil, err + } + } + return nil, nil + }) + return cmd.err +} + +//------------------------------------------------------------------------------ + +type CommandInfo struct { + Name string + Arity int8 + Flags []string + ACLFlags []string + FirstKeyPos int8 + LastKeyPos int8 + StepCount int8 + ReadOnly bool +} + +type CommandsInfoCmd struct { + baseCmd + + val map[string]*CommandInfo +} + +var _ Cmder = (*CommandsInfoCmd)(nil) + +func NewCommandsInfoCmd(ctx context.Context, args ...interface{}) *CommandsInfoCmd { + return &CommandsInfoCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *CommandsInfoCmd) Val() map[string]*CommandInfo { + return cmd.val +} + +func (cmd *CommandsInfoCmd) Result() (map[string]*CommandInfo, error) { + return cmd.Val(), cmd.Err() +} + +func (cmd *CommandsInfoCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *CommandsInfoCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make(map[string]*CommandInfo, n) + for i := int64(0); i < n; i++ { + v, err := rd.ReadReply(commandInfoParser) + if err != nil { + return nil, err + } + vv := v.(*CommandInfo) + cmd.val[vv.Name] = vv + } + return nil, nil + }) + return cmd.err +} + +func commandInfoParser(rd *proto.Reader, n int64) (interface{}, error) { + const numArgRedis5 = 6 + const numArgRedis6 = 7 + + switch n { + case numArgRedis5, numArgRedis6: + // continue + default: + return nil, fmt.Errorf("redis: got %d elements in COMMAND reply, wanted 7", n) + } + + var cmd CommandInfo + var err error + + cmd.Name, err = rd.ReadString() + if err != nil { + return nil, err + } + + arity, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + cmd.Arity = int8(arity) + + _, err = rd.ReadReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.Flags = make([]string, n) + for i := 0; i < len(cmd.Flags); i++ { + switch s, err := rd.ReadString(); { + case err == Nil: + cmd.Flags[i] = "" + case err != nil: + return nil, err + default: + cmd.Flags[i] = s + } + } + return nil, nil + }) + if err != nil { + return nil, err + } + + firstKeyPos, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + cmd.FirstKeyPos = int8(firstKeyPos) + + lastKeyPos, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + cmd.LastKeyPos = int8(lastKeyPos) + + stepCount, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + cmd.StepCount = int8(stepCount) + + for _, flag := range cmd.Flags { + if flag == "readonly" { + cmd.ReadOnly = true + break + } + } + + if n == numArgRedis5 { + return &cmd, nil + } + + _, err = rd.ReadReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.ACLFlags = make([]string, n) + for i := 0; i < len(cmd.ACLFlags); i++ { + switch s, err := rd.ReadString(); { + case err == Nil: + cmd.ACLFlags[i] = "" + case err != nil: + return nil, err + default: + cmd.ACLFlags[i] = s + } + } + return nil, nil + }) + if err != nil { + return nil, err + } + + return &cmd, nil +} + +//------------------------------------------------------------------------------ + +type cmdsInfoCache struct { + fn func() (map[string]*CommandInfo, error) + + once internal.Once + cmds map[string]*CommandInfo +} + +func newCmdsInfoCache(fn func() (map[string]*CommandInfo, error)) *cmdsInfoCache { + return &cmdsInfoCache{ + fn: fn, + } +} + +func (c *cmdsInfoCache) Get() (map[string]*CommandInfo, error) { + err := c.once.Do(func() error { + cmds, err := c.fn() + if err != nil { + return err + } + + // Extensions have cmd names in upper case. Convert them to lower case. + for k, v := range cmds { + lower := internal.ToLower(k) + if lower != k { + cmds[lower] = v + } + } + + c.cmds = cmds + return nil + }) + return c.cmds, err +} + +//------------------------------------------------------------------------------ + +type SlowLog struct { + ID int64 + Time time.Time + Duration time.Duration + Args []string + // These are also optional fields emitted only by Redis 4.0 or greater: + // https://redis.io/commands/slowlog#output-format + ClientAddr string + ClientName string +} + +type SlowLogCmd struct { + baseCmd + + val []SlowLog +} + +var _ Cmder = (*SlowLogCmd)(nil) + +func NewSlowLogCmd(ctx context.Context, args ...interface{}) *SlowLogCmd { + return &SlowLogCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *SlowLogCmd) Val() []SlowLog { + return cmd.val +} + +func (cmd *SlowLogCmd) Result() ([]SlowLog, error) { + return cmd.Val(), cmd.Err() +} + +func (cmd *SlowLogCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *SlowLogCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]SlowLog, n) + for i := 0; i < len(cmd.val); i++ { + n, err := rd.ReadArrayLen() + if err != nil { + return nil, err + } + if n < 4 { + err := fmt.Errorf("redis: got %d elements in slowlog get, expected at least 4", n) + return nil, err + } + + id, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + + createdAt, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + createdAtTime := time.Unix(createdAt, 0) + + costs, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + costsDuration := time.Duration(costs) * time.Microsecond + + cmdLen, err := rd.ReadArrayLen() + if err != nil { + return nil, err + } + if cmdLen < 1 { + err := fmt.Errorf("redis: got %d elements commands reply in slowlog get, expected at least 1", cmdLen) + return nil, err + } + + cmdString := make([]string, cmdLen) + for i := 0; i < int(cmdLen); i++ { + cmdString[i], err = rd.ReadString() + if err != nil { + return nil, err + } + } + + var address, name string + for i := 4; i < int(n); i++ { + str, err := rd.ReadString() + if err != nil { + return nil, err + } + if i == 4 { + address = str + } else if i == 5 { + name = str + } + } + + cmd.val[i] = SlowLog{ + ID: id, + Time: createdAtTime, + Duration: costsDuration, + Args: cmdString, + ClientAddr: address, + ClientName: name, + } + } + return nil, nil + }) + return cmd.err +} diff --git a/vendor/github.com/go-redis/redis/v8/commands.go b/vendor/github.com/go-redis/redis/v8/commands.go new file mode 100644 index 0000000000..5ee2836fa7 --- /dev/null +++ b/vendor/github.com/go-redis/redis/v8/commands.go @@ -0,0 +1,2690 @@ +package redis + +import ( + "context" + "errors" + "io" + "time" + + "github.com/go-redis/redis/v8/internal" +) + +func usePrecise(dur time.Duration) bool { + return dur < time.Second || dur%time.Second != 0 +} + +func formatMs(ctx context.Context, dur time.Duration) int64 { + if dur > 0 && dur < time.Millisecond { + internal.Logger.Printf( + ctx, + "specified duration is %s, but minimal supported value is %s - truncating to 1ms", + dur, time.Millisecond, + ) + return 1 + } + return int64(dur / time.Millisecond) +} + +func formatSec(ctx context.Context, dur time.Duration) int64 { + if dur > 0 && dur < time.Second { + internal.Logger.Printf( + ctx, + "specified duration is %s, but minimal supported value is %s - truncating to 1s", + dur, time.Second, + ) + return 1 + } + return int64(dur / time.Second) +} + +func appendArgs(dst, src []interface{}) []interface{} { + if len(src) == 1 { + return appendArg(dst, src[0]) + } + + dst = append(dst, src...) + return dst +} + +func appendArg(dst []interface{}, arg interface{}) []interface{} { + switch arg := arg.(type) { + case []string: + for _, s := range arg { + dst = append(dst, s) + } + return dst + case []interface{}: + dst = append(dst, arg...) + return dst + case map[string]interface{}: + for k, v := range arg { + dst = append(dst, k, v) + } + return dst + default: + return append(dst, arg) + } +} + +type Cmdable interface { + Pipeline() Pipeliner + Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) + + TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) + TxPipeline() Pipeliner + + Command(ctx context.Context) *CommandsInfoCmd + ClientGetName(ctx context.Context) *StringCmd + Echo(ctx context.Context, message interface{}) *StringCmd + Ping(ctx context.Context) *StatusCmd + Quit(ctx context.Context) *StatusCmd + Del(ctx context.Context, keys ...string) *IntCmd + Unlink(ctx context.Context, keys ...string) *IntCmd + Dump(ctx context.Context, key string) *StringCmd + Exists(ctx context.Context, keys ...string) *IntCmd + Expire(ctx context.Context, key string, expiration time.Duration) *BoolCmd + ExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd + Keys(ctx context.Context, pattern string) *StringSliceCmd + Migrate(ctx context.Context, host, port, key string, db int, timeout time.Duration) *StatusCmd + Move(ctx context.Context, key string, db int) *BoolCmd + ObjectRefCount(ctx context.Context, key string) *IntCmd + ObjectEncoding(ctx context.Context, key string) *StringCmd + ObjectIdleTime(ctx context.Context, key string) *DurationCmd + Persist(ctx context.Context, key string) *BoolCmd + PExpire(ctx context.Context, key string, expiration time.Duration) *BoolCmd + PExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd + PTTL(ctx context.Context, key string) *DurationCmd + RandomKey(ctx context.Context) *StringCmd + Rename(ctx context.Context, key, newkey string) *StatusCmd + RenameNX(ctx context.Context, key, newkey string) *BoolCmd + Restore(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd + RestoreReplace(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd + Sort(ctx context.Context, key string, sort *Sort) *StringSliceCmd + SortStore(ctx context.Context, key, store string, sort *Sort) *IntCmd + SortInterfaces(ctx context.Context, key string, sort *Sort) *SliceCmd + Touch(ctx context.Context, keys ...string) *IntCmd + TTL(ctx context.Context, key string) *DurationCmd + Type(ctx context.Context, key string) *StatusCmd + Append(ctx context.Context, key, value string) *IntCmd + Decr(ctx context.Context, key string) *IntCmd + DecrBy(ctx context.Context, key string, decrement int64) *IntCmd + Get(ctx context.Context, key string) *StringCmd + GetRange(ctx context.Context, key string, start, end int64) *StringCmd + GetSet(ctx context.Context, key string, value interface{}) *StringCmd + Incr(ctx context.Context, key string) *IntCmd + IncrBy(ctx context.Context, key string, value int64) *IntCmd + IncrByFloat(ctx context.Context, key string, value float64) *FloatCmd + MGet(ctx context.Context, keys ...string) *SliceCmd + MSet(ctx context.Context, values ...interface{}) *StatusCmd + MSetNX(ctx context.Context, values ...interface{}) *BoolCmd + Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd + SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd + SetXX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd + SetRange(ctx context.Context, key string, offset int64, value string) *IntCmd + StrLen(ctx context.Context, key string) *IntCmd + + GetBit(ctx context.Context, key string, offset int64) *IntCmd + SetBit(ctx context.Context, key string, offset int64, value int) *IntCmd + BitCount(ctx context.Context, key string, bitCount *BitCount) *IntCmd + BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd + BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd + BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd + BitOpNot(ctx context.Context, destKey string, key string) *IntCmd + BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd + BitField(ctx context.Context, key string, args ...interface{}) *IntSliceCmd + + Scan(ctx context.Context, cursor uint64, match string, count int64) *ScanCmd + SScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd + HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd + ZScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd + + HDel(ctx context.Context, key string, fields ...string) *IntCmd + HExists(ctx context.Context, key, field string) *BoolCmd + HGet(ctx context.Context, key, field string) *StringCmd + HGetAll(ctx context.Context, key string) *StringStringMapCmd + HIncrBy(ctx context.Context, key, field string, incr int64) *IntCmd + HIncrByFloat(ctx context.Context, key, field string, incr float64) *FloatCmd + HKeys(ctx context.Context, key string) *StringSliceCmd + HLen(ctx context.Context, key string) *IntCmd + HMGet(ctx context.Context, key string, fields ...string) *SliceCmd + HSet(ctx context.Context, key string, values ...interface{}) *IntCmd + HMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd + HSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd + HVals(ctx context.Context, key string) *StringSliceCmd + + BLPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd + BRPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd + BRPopLPush(ctx context.Context, source, destination string, timeout time.Duration) *StringCmd + LIndex(ctx context.Context, key string, index int64) *StringCmd + LInsert(ctx context.Context, key, op string, pivot, value interface{}) *IntCmd + LInsertBefore(ctx context.Context, key string, pivot, value interface{}) *IntCmd + LInsertAfter(ctx context.Context, key string, pivot, value interface{}) *IntCmd + LLen(ctx context.Context, key string) *IntCmd + LPop(ctx context.Context, key string) *StringCmd + LPush(ctx context.Context, key string, values ...interface{}) *IntCmd + LPushX(ctx context.Context, key string, values ...interface{}) *IntCmd + LRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd + LRem(ctx context.Context, key string, count int64, value interface{}) *IntCmd + LSet(ctx context.Context, key string, index int64, value interface{}) *StatusCmd + LTrim(ctx context.Context, key string, start, stop int64) *StatusCmd + RPop(ctx context.Context, key string) *StringCmd + RPopLPush(ctx context.Context, source, destination string) *StringCmd + RPush(ctx context.Context, key string, values ...interface{}) *IntCmd + RPushX(ctx context.Context, key string, values ...interface{}) *IntCmd + + SAdd(ctx context.Context, key string, members ...interface{}) *IntCmd + SCard(ctx context.Context, key string) *IntCmd + SDiff(ctx context.Context, keys ...string) *StringSliceCmd + SDiffStore(ctx context.Context, destination string, keys ...string) *IntCmd + SInter(ctx context.Context, keys ...string) *StringSliceCmd + SInterStore(ctx context.Context, destination string, keys ...string) *IntCmd + SIsMember(ctx context.Context, key string, member interface{}) *BoolCmd + SMembers(ctx context.Context, key string) *StringSliceCmd + SMembersMap(ctx context.Context, key string) *StringStructMapCmd + SMove(ctx context.Context, source, destination string, member interface{}) *BoolCmd + SPop(ctx context.Context, key string) *StringCmd + SPopN(ctx context.Context, key string, count int64) *StringSliceCmd + SRandMember(ctx context.Context, key string) *StringCmd + SRandMemberN(ctx context.Context, key string, count int64) *StringSliceCmd + SRem(ctx context.Context, key string, members ...interface{}) *IntCmd + SUnion(ctx context.Context, keys ...string) *StringSliceCmd + SUnionStore(ctx context.Context, destination string, keys ...string) *IntCmd + + XAdd(ctx context.Context, a *XAddArgs) *StringCmd + XDel(ctx context.Context, stream string, ids ...string) *IntCmd + XLen(ctx context.Context, stream string) *IntCmd + XRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd + XRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd + XRevRange(ctx context.Context, stream string, start, stop string) *XMessageSliceCmd + XRevRangeN(ctx context.Context, stream string, start, stop string, count int64) *XMessageSliceCmd + XRead(ctx context.Context, a *XReadArgs) *XStreamSliceCmd + XReadStreams(ctx context.Context, streams ...string) *XStreamSliceCmd + XGroupCreate(ctx context.Context, stream, group, start string) *StatusCmd + XGroupCreateMkStream(ctx context.Context, stream, group, start string) *StatusCmd + XGroupSetID(ctx context.Context, stream, group, start string) *StatusCmd + XGroupDestroy(ctx context.Context, stream, group string) *IntCmd + XGroupDelConsumer(ctx context.Context, stream, group, consumer string) *IntCmd + XReadGroup(ctx context.Context, a *XReadGroupArgs) *XStreamSliceCmd + XAck(ctx context.Context, stream, group string, ids ...string) *IntCmd + XPending(ctx context.Context, stream, group string) *XPendingCmd + XPendingExt(ctx context.Context, a *XPendingExtArgs) *XPendingExtCmd + XClaim(ctx context.Context, a *XClaimArgs) *XMessageSliceCmd + XClaimJustID(ctx context.Context, a *XClaimArgs) *StringSliceCmd + XTrim(ctx context.Context, key string, maxLen int64) *IntCmd + XTrimApprox(ctx context.Context, key string, maxLen int64) *IntCmd + XInfoGroups(ctx context.Context, key string) *XInfoGroupsCmd + + BZPopMax(ctx context.Context, timeout time.Duration, keys ...string) *ZWithKeyCmd + BZPopMin(ctx context.Context, timeout time.Duration, keys ...string) *ZWithKeyCmd + ZAdd(ctx context.Context, key string, members ...*Z) *IntCmd + ZAddNX(ctx context.Context, key string, members ...*Z) *IntCmd + ZAddXX(ctx context.Context, key string, members ...*Z) *IntCmd + ZAddCh(ctx context.Context, key string, members ...*Z) *IntCmd + ZAddNXCh(ctx context.Context, key string, members ...*Z) *IntCmd + ZAddXXCh(ctx context.Context, key string, members ...*Z) *IntCmd + ZIncr(ctx context.Context, key string, member *Z) *FloatCmd + ZIncrNX(ctx context.Context, key string, member *Z) *FloatCmd + ZIncrXX(ctx context.Context, key string, member *Z) *FloatCmd + ZCard(ctx context.Context, key string) *IntCmd + ZCount(ctx context.Context, key, min, max string) *IntCmd + ZLexCount(ctx context.Context, key, min, max string) *IntCmd + ZIncrBy(ctx context.Context, key string, increment float64, member string) *FloatCmd + ZInterStore(ctx context.Context, destination string, store *ZStore) *IntCmd + ZPopMax(ctx context.Context, key string, count ...int64) *ZSliceCmd + ZPopMin(ctx context.Context, key string, count ...int64) *ZSliceCmd + ZRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd + ZRangeWithScores(ctx context.Context, key string, start, stop int64) *ZSliceCmd + ZRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd + ZRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd + ZRangeByScoreWithScores(ctx context.Context, key string, opt *ZRangeBy) *ZSliceCmd + ZRank(ctx context.Context, key, member string) *IntCmd + ZRem(ctx context.Context, key string, members ...interface{}) *IntCmd + ZRemRangeByRank(ctx context.Context, key string, start, stop int64) *IntCmd + ZRemRangeByScore(ctx context.Context, key, min, max string) *IntCmd + ZRemRangeByLex(ctx context.Context, key, min, max string) *IntCmd + ZRevRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd + ZRevRangeWithScores(ctx context.Context, key string, start, stop int64) *ZSliceCmd + ZRevRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd + ZRevRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd + ZRevRangeByScoreWithScores(ctx context.Context, key string, opt *ZRangeBy) *ZSliceCmd + ZRevRank(ctx context.Context, key, member string) *IntCmd + ZScore(ctx context.Context, key, member string) *FloatCmd + ZUnionStore(ctx context.Context, dest string, store *ZStore) *IntCmd + + PFAdd(ctx context.Context, key string, els ...interface{}) *IntCmd + PFCount(ctx context.Context, keys ...string) *IntCmd + PFMerge(ctx context.Context, dest string, keys ...string) *StatusCmd + + BgRewriteAOF(ctx context.Context) *StatusCmd + BgSave(ctx context.Context) *StatusCmd + ClientKill(ctx context.Context, ipPort string) *StatusCmd + ClientKillByFilter(ctx context.Context, keys ...string) *IntCmd + ClientList(ctx context.Context) *StringCmd + ClientPause(ctx context.Context, dur time.Duration) *BoolCmd + ClientID(ctx context.Context) *IntCmd + ConfigGet(ctx context.Context, parameter string) *SliceCmd + ConfigResetStat(ctx context.Context) *StatusCmd + ConfigSet(ctx context.Context, parameter, value string) *StatusCmd + ConfigRewrite(ctx context.Context) *StatusCmd + DBSize(ctx context.Context) *IntCmd + FlushAll(ctx context.Context) *StatusCmd + FlushAllAsync(ctx context.Context) *StatusCmd + FlushDB(ctx context.Context) *StatusCmd + FlushDBAsync(ctx context.Context) *StatusCmd + Info(ctx context.Context, section ...string) *StringCmd + LastSave(ctx context.Context) *IntCmd + Save(ctx context.Context) *StatusCmd + Shutdown(ctx context.Context) *StatusCmd + ShutdownSave(ctx context.Context) *StatusCmd + ShutdownNoSave(ctx context.Context) *StatusCmd + SlaveOf(ctx context.Context, host, port string) *StatusCmd + Time(ctx context.Context) *TimeCmd + DebugObject(ctx context.Context, key string) *StringCmd + ReadOnly(ctx context.Context) *StatusCmd + ReadWrite(ctx context.Context) *StatusCmd + MemoryUsage(ctx context.Context, key string, samples ...int) *IntCmd + + Eval(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd + EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd + ScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd + ScriptFlush(ctx context.Context) *StatusCmd + ScriptKill(ctx context.Context) *StatusCmd + ScriptLoad(ctx context.Context, script string) *StringCmd + + Publish(ctx context.Context, channel string, message interface{}) *IntCmd + PubSubChannels(ctx context.Context, pattern string) *StringSliceCmd + PubSubNumSub(ctx context.Context, channels ...string) *StringIntMapCmd + PubSubNumPat(ctx context.Context) *IntCmd + + ClusterSlots(ctx context.Context) *ClusterSlotsCmd + ClusterNodes(ctx context.Context) *StringCmd + ClusterMeet(ctx context.Context, host, port string) *StatusCmd + ClusterForget(ctx context.Context, nodeID string) *StatusCmd + ClusterReplicate(ctx context.Context, nodeID string) *StatusCmd + ClusterResetSoft(ctx context.Context) *StatusCmd + ClusterResetHard(ctx context.Context) *StatusCmd + ClusterInfo(ctx context.Context) *StringCmd + ClusterKeySlot(ctx context.Context, key string) *IntCmd + ClusterGetKeysInSlot(ctx context.Context, slot int, count int) *StringSliceCmd + ClusterCountFailureReports(ctx context.Context, nodeID string) *IntCmd + ClusterCountKeysInSlot(ctx context.Context, slot int) *IntCmd + ClusterDelSlots(ctx context.Context, slots ...int) *StatusCmd + ClusterDelSlotsRange(ctx context.Context, min, max int) *StatusCmd + ClusterSaveConfig(ctx context.Context) *StatusCmd + ClusterSlaves(ctx context.Context, nodeID string) *StringSliceCmd + ClusterFailover(ctx context.Context) *StatusCmd + ClusterAddSlots(ctx context.Context, slots ...int) *StatusCmd + ClusterAddSlotsRange(ctx context.Context, min, max int) *StatusCmd + + GeoAdd(ctx context.Context, key string, geoLocation ...*GeoLocation) *IntCmd + GeoPos(ctx context.Context, key string, members ...string) *GeoPosCmd + GeoRadius(ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery) *GeoLocationCmd + GeoRadiusStore(ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery) *IntCmd + GeoRadiusByMember(ctx context.Context, key, member string, query *GeoRadiusQuery) *GeoLocationCmd + GeoRadiusByMemberStore(ctx context.Context, key, member string, query *GeoRadiusQuery) *IntCmd + GeoDist(ctx context.Context, key string, member1, member2, unit string) *FloatCmd + GeoHash(ctx context.Context, key string, members ...string) *StringSliceCmd +} + +type StatefulCmdable interface { + Cmdable + Auth(ctx context.Context, password string) *StatusCmd + AuthACL(ctx context.Context, username, password string) *StatusCmd + Select(ctx context.Context, index int) *StatusCmd + SwapDB(ctx context.Context, index1, index2 int) *StatusCmd + ClientSetName(ctx context.Context, name string) *BoolCmd +} + +var ( + _ Cmdable = (*Client)(nil) + _ Cmdable = (*Tx)(nil) + _ Cmdable = (*Ring)(nil) + _ Cmdable = (*ClusterClient)(nil) +) + +type cmdable func(ctx context.Context, cmd Cmder) error + +type statefulCmdable func(ctx context.Context, cmd Cmder) error + +//------------------------------------------------------------------------------ + +func (c statefulCmdable) Auth(ctx context.Context, password string) *StatusCmd { + cmd := NewStatusCmd(ctx, "auth", password) + _ = c(ctx, cmd) + return cmd +} + +// Perform an AUTH command, using the given user and pass. +// Should be used to authenticate the current connection with one of the connections defined in the ACL list +// when connecting to a Redis 6.0 instance, or greater, that is using the Redis ACL system. +func (c statefulCmdable) AuthACL(ctx context.Context, username, password string) *StatusCmd { + cmd := NewStatusCmd(ctx, "auth", username, password) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Wait(ctx context.Context, numSlaves int, timeout time.Duration) *IntCmd { + cmd := NewIntCmd(ctx, "wait", numSlaves, int(timeout/time.Millisecond)) + _ = c(ctx, cmd) + return cmd +} + +func (c statefulCmdable) Select(ctx context.Context, index int) *StatusCmd { + cmd := NewStatusCmd(ctx, "select", index) + _ = c(ctx, cmd) + return cmd +} + +func (c statefulCmdable) SwapDB(ctx context.Context, index1, index2 int) *StatusCmd { + cmd := NewStatusCmd(ctx, "swapdb", index1, index2) + _ = c(ctx, cmd) + return cmd +} + +// ClientSetName assigns a name to the connection. +func (c statefulCmdable) ClientSetName(ctx context.Context, name string) *BoolCmd { + cmd := NewBoolCmd(ctx, "client", "setname", name) + _ = c(ctx, cmd) + return cmd +} + +//------------------------------------------------------------------------------ + +func (c cmdable) Command(ctx context.Context) *CommandsInfoCmd { + cmd := NewCommandsInfoCmd(ctx, "command") + _ = c(ctx, cmd) + return cmd +} + +// ClientGetName returns the name of the connection. +func (c cmdable) ClientGetName(ctx context.Context) *StringCmd { + cmd := NewStringCmd(ctx, "client", "getname") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Echo(ctx context.Context, message interface{}) *StringCmd { + cmd := NewStringCmd(ctx, "echo", message) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Ping(ctx context.Context) *StatusCmd { + cmd := NewStatusCmd(ctx, "ping") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Quit(ctx context.Context) *StatusCmd { + panic("not implemented") +} + +func (c cmdable) Del(ctx context.Context, keys ...string) *IntCmd { + args := make([]interface{}, 1+len(keys)) + args[0] = "del" + for i, key := range keys { + args[1+i] = key + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Unlink(ctx context.Context, keys ...string) *IntCmd { + args := make([]interface{}, 1+len(keys)) + args[0] = "unlink" + for i, key := range keys { + args[1+i] = key + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Dump(ctx context.Context, key string) *StringCmd { + cmd := NewStringCmd(ctx, "dump", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Exists(ctx context.Context, keys ...string) *IntCmd { + args := make([]interface{}, 1+len(keys)) + args[0] = "exists" + for i, key := range keys { + args[1+i] = key + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Expire(ctx context.Context, key string, expiration time.Duration) *BoolCmd { + cmd := NewBoolCmd(ctx, "expire", key, formatSec(ctx, expiration)) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd { + cmd := NewBoolCmd(ctx, "expireat", key, tm.Unix()) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Keys(ctx context.Context, pattern string) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "keys", pattern) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Migrate(ctx context.Context, host, port, key string, db int, timeout time.Duration) *StatusCmd { + cmd := NewStatusCmd( + ctx, + "migrate", + host, + port, + key, + db, + formatMs(ctx, timeout), + ) + cmd.setReadTimeout(timeout) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Move(ctx context.Context, key string, db int) *BoolCmd { + cmd := NewBoolCmd(ctx, "move", key, db) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ObjectRefCount(ctx context.Context, key string) *IntCmd { + cmd := NewIntCmd(ctx, "object", "refcount", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ObjectEncoding(ctx context.Context, key string) *StringCmd { + cmd := NewStringCmd(ctx, "object", "encoding", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ObjectIdleTime(ctx context.Context, key string) *DurationCmd { + cmd := NewDurationCmd(ctx, time.Second, "object", "idletime", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Persist(ctx context.Context, key string) *BoolCmd { + cmd := NewBoolCmd(ctx, "persist", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) PExpire(ctx context.Context, key string, expiration time.Duration) *BoolCmd { + cmd := NewBoolCmd(ctx, "pexpire", key, formatMs(ctx, expiration)) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) PExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd { + cmd := NewBoolCmd( + ctx, + "pexpireat", + key, + tm.UnixNano()/int64(time.Millisecond), + ) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) PTTL(ctx context.Context, key string) *DurationCmd { + cmd := NewDurationCmd(ctx, time.Millisecond, "pttl", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) RandomKey(ctx context.Context) *StringCmd { + cmd := NewStringCmd(ctx, "randomkey") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Rename(ctx context.Context, key, newkey string) *StatusCmd { + cmd := NewStatusCmd(ctx, "rename", key, newkey) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) RenameNX(ctx context.Context, key, newkey string) *BoolCmd { + cmd := NewBoolCmd(ctx, "renamenx", key, newkey) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Restore(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd { + cmd := NewStatusCmd( + ctx, + "restore", + key, + formatMs(ctx, ttl), + value, + ) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) RestoreReplace(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd { + cmd := NewStatusCmd( + ctx, + "restore", + key, + formatMs(ctx, ttl), + value, + "replace", + ) + _ = c(ctx, cmd) + return cmd +} + +type Sort struct { + By string + Offset, Count int64 + Get []string + Order string + Alpha bool +} + +func (sort *Sort) args(key string) []interface{} { + args := []interface{}{"sort", key} + if sort.By != "" { + args = append(args, "by", sort.By) + } + if sort.Offset != 0 || sort.Count != 0 { + args = append(args, "limit", sort.Offset, sort.Count) + } + for _, get := range sort.Get { + args = append(args, "get", get) + } + if sort.Order != "" { + args = append(args, sort.Order) + } + if sort.Alpha { + args = append(args, "alpha") + } + return args +} + +func (c cmdable) Sort(ctx context.Context, key string, sort *Sort) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, sort.args(key)...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SortStore(ctx context.Context, key, store string, sort *Sort) *IntCmd { + args := sort.args(key) + if store != "" { + args = append(args, "store", store) + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SortInterfaces(ctx context.Context, key string, sort *Sort) *SliceCmd { + cmd := NewSliceCmd(ctx, sort.args(key)...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Touch(ctx context.Context, keys ...string) *IntCmd { + args := make([]interface{}, len(keys)+1) + args[0] = "touch" + for i, key := range keys { + args[i+1] = key + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) TTL(ctx context.Context, key string) *DurationCmd { + cmd := NewDurationCmd(ctx, time.Second, "ttl", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Type(ctx context.Context, key string) *StatusCmd { + cmd := NewStatusCmd(ctx, "type", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Append(ctx context.Context, key, value string) *IntCmd { + cmd := NewIntCmd(ctx, "append", key, value) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Decr(ctx context.Context, key string) *IntCmd { + cmd := NewIntCmd(ctx, "decr", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) DecrBy(ctx context.Context, key string, decrement int64) *IntCmd { + cmd := NewIntCmd(ctx, "decrby", key, decrement) + _ = c(ctx, cmd) + return cmd +} + +// Redis `GET key` command. It returns redis.Nil error when key does not exist. +func (c cmdable) Get(ctx context.Context, key string) *StringCmd { + cmd := NewStringCmd(ctx, "get", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) GetRange(ctx context.Context, key string, start, end int64) *StringCmd { + cmd := NewStringCmd(ctx, "getrange", key, start, end) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) GetSet(ctx context.Context, key string, value interface{}) *StringCmd { + cmd := NewStringCmd(ctx, "getset", key, value) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) Incr(ctx context.Context, key string) *IntCmd { + cmd := NewIntCmd(ctx, "incr", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) IncrBy(ctx context.Context, key string, value int64) *IntCmd { + cmd := NewIntCmd(ctx, "incrby", key, value) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) IncrByFloat(ctx context.Context, key string, value float64) *FloatCmd { + cmd := NewFloatCmd(ctx, "incrbyfloat", key, value) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) MGet(ctx context.Context, keys ...string) *SliceCmd { + args := make([]interface{}, 1+len(keys)) + args[0] = "mget" + for i, key := range keys { + args[1+i] = key + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// MSet is like Set but accepts multiple values: +// - MSet("key1", "value1", "key2", "value2") +// - MSet([]string{"key1", "value1", "key2", "value2"}) +// - MSet(map[string]interface{}{"key1": "value1", "key2": "value2"}) +func (c cmdable) MSet(ctx context.Context, values ...interface{}) *StatusCmd { + args := make([]interface{}, 1, 1+len(values)) + args[0] = "mset" + args = appendArgs(args, values) + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// MSetNX is like SetNX but accepts multiple values: +// - MSetNX("key1", "value1", "key2", "value2") +// - MSetNX([]string{"key1", "value1", "key2", "value2"}) +// - MSetNX(map[string]interface{}{"key1": "value1", "key2": "value2"}) +func (c cmdable) MSetNX(ctx context.Context, values ...interface{}) *BoolCmd { + args := make([]interface{}, 1, 1+len(values)) + args[0] = "msetnx" + args = appendArgs(args, values) + cmd := NewBoolCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// Redis `SET key value [expiration]` command. +// +// Use expiration for `SETEX`-like behavior. +// Zero expiration means the key has no expiration time. +func (c cmdable) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd { + args := make([]interface{}, 3, 5) + args[0] = "set" + args[1] = key + args[2] = value + if expiration > 0 { + if usePrecise(expiration) { + args = append(args, "px", formatMs(ctx, expiration)) + } else { + args = append(args, "ex", formatSec(ctx, expiration)) + } + } + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// Redis `SET key value [expiration] NX` command. +// +// Zero expiration means the key has no expiration time. +func (c cmdable) SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd { + var cmd *BoolCmd + if expiration == 0 { + // Use old `SETNX` to support old Redis versions. + cmd = NewBoolCmd(ctx, "setnx", key, value) + } else { + if usePrecise(expiration) { + cmd = NewBoolCmd(ctx, "set", key, value, "px", formatMs(ctx, expiration), "nx") + } else { + cmd = NewBoolCmd(ctx, "set", key, value, "ex", formatSec(ctx, expiration), "nx") + } + } + _ = c(ctx, cmd) + return cmd +} + +// Redis `SET key value [expiration] XX` command. +// +// Zero expiration means the key has no expiration time. +func (c cmdable) SetXX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd { + var cmd *BoolCmd + if expiration == 0 { + cmd = NewBoolCmd(ctx, "set", key, value, "xx") + } else { + if usePrecise(expiration) { + cmd = NewBoolCmd(ctx, "set", key, value, "px", formatMs(ctx, expiration), "xx") + } else { + cmd = NewBoolCmd(ctx, "set", key, value, "ex", formatSec(ctx, expiration), "xx") + } + } + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SetRange(ctx context.Context, key string, offset int64, value string) *IntCmd { + cmd := NewIntCmd(ctx, "setrange", key, offset, value) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) StrLen(ctx context.Context, key string) *IntCmd { + cmd := NewIntCmd(ctx, "strlen", key) + _ = c(ctx, cmd) + return cmd +} + +//------------------------------------------------------------------------------ + +func (c cmdable) GetBit(ctx context.Context, key string, offset int64) *IntCmd { + cmd := NewIntCmd(ctx, "getbit", key, offset) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SetBit(ctx context.Context, key string, offset int64, value int) *IntCmd { + cmd := NewIntCmd( + ctx, + "setbit", + key, + offset, + value, + ) + _ = c(ctx, cmd) + return cmd +} + +type BitCount struct { + Start, End int64 +} + +func (c cmdable) BitCount(ctx context.Context, key string, bitCount *BitCount) *IntCmd { + args := []interface{}{"bitcount", key} + if bitCount != nil { + args = append( + args, + bitCount.Start, + bitCount.End, + ) + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) bitOp(ctx context.Context, op, destKey string, keys ...string) *IntCmd { + args := make([]interface{}, 3+len(keys)) + args[0] = "bitop" + args[1] = op + args[2] = destKey + for i, key := range keys { + args[3+i] = key + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd { + return c.bitOp(ctx, "and", destKey, keys...) +} + +func (c cmdable) BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd { + return c.bitOp(ctx, "or", destKey, keys...) +} + +func (c cmdable) BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd { + return c.bitOp(ctx, "xor", destKey, keys...) +} + +func (c cmdable) BitOpNot(ctx context.Context, destKey string, key string) *IntCmd { + return c.bitOp(ctx, "not", destKey, key) +} + +func (c cmdable) BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd { + args := make([]interface{}, 3+len(pos)) + args[0] = "bitpos" + args[1] = key + args[2] = bit + switch len(pos) { + case 0: + case 1: + args[3] = pos[0] + case 2: + args[3] = pos[0] + args[4] = pos[1] + default: + panic("too many arguments") + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) BitField(ctx context.Context, key string, args ...interface{}) *IntSliceCmd { + a := make([]interface{}, 0, 2+len(args)) + a = append(a, "bitfield") + a = append(a, key) + a = append(a, args...) + cmd := NewIntSliceCmd(ctx, a...) + _ = c(ctx, cmd) + return cmd +} + +//------------------------------------------------------------------------------ + +func (c cmdable) Scan(ctx context.Context, cursor uint64, match string, count int64) *ScanCmd { + args := []interface{}{"scan", cursor} + if match != "" { + args = append(args, "match", match) + } + if count > 0 { + args = append(args, "count", count) + } + cmd := NewScanCmd(ctx, c, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd { + args := []interface{}{"sscan", key, cursor} + if match != "" { + args = append(args, "match", match) + } + if count > 0 { + args = append(args, "count", count) + } + cmd := NewScanCmd(ctx, c, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd { + args := []interface{}{"hscan", key, cursor} + if match != "" { + args = append(args, "match", match) + } + if count > 0 { + args = append(args, "count", count) + } + cmd := NewScanCmd(ctx, c, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd { + args := []interface{}{"zscan", key, cursor} + if match != "" { + args = append(args, "match", match) + } + if count > 0 { + args = append(args, "count", count) + } + cmd := NewScanCmd(ctx, c, args...) + _ = c(ctx, cmd) + return cmd +} + +//------------------------------------------------------------------------------ + +func (c cmdable) HDel(ctx context.Context, key string, fields ...string) *IntCmd { + args := make([]interface{}, 2+len(fields)) + args[0] = "hdel" + args[1] = key + for i, field := range fields { + args[2+i] = field + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HExists(ctx context.Context, key, field string) *BoolCmd { + cmd := NewBoolCmd(ctx, "hexists", key, field) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HGet(ctx context.Context, key, field string) *StringCmd { + cmd := NewStringCmd(ctx, "hget", key, field) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HGetAll(ctx context.Context, key string) *StringStringMapCmd { + cmd := NewStringStringMapCmd(ctx, "hgetall", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HIncrBy(ctx context.Context, key, field string, incr int64) *IntCmd { + cmd := NewIntCmd(ctx, "hincrby", key, field, incr) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HIncrByFloat(ctx context.Context, key, field string, incr float64) *FloatCmd { + cmd := NewFloatCmd(ctx, "hincrbyfloat", key, field, incr) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HKeys(ctx context.Context, key string) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "hkeys", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HLen(ctx context.Context, key string) *IntCmd { + cmd := NewIntCmd(ctx, "hlen", key) + _ = c(ctx, cmd) + return cmd +} + +// HMGet returns the values for the specified fields in the hash stored at key. +// It returns an interface{} to distinguish between empty string and nil value. +func (c cmdable) HMGet(ctx context.Context, key string, fields ...string) *SliceCmd { + args := make([]interface{}, 2+len(fields)) + args[0] = "hmget" + args[1] = key + for i, field := range fields { + args[2+i] = field + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HSet accepts values in following formats: +// - HSet("myhash", "key1", "value1", "key2", "value2") +// - HSet("myhash", []string{"key1", "value1", "key2", "value2"}) +// - HSet("myhash", map[string]interface{}{"key1": "value1", "key2": "value2"}) +// +// Note that it requires Redis v4 for multiple field/value pairs support. +func (c cmdable) HSet(ctx context.Context, key string, values ...interface{}) *IntCmd { + args := make([]interface{}, 2, 2+len(values)) + args[0] = "hset" + args[1] = key + args = appendArgs(args, values) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HMSet is a deprecated version of HSet left for compatibility with Redis 3. +func (c cmdable) HMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd { + args := make([]interface{}, 2, 2+len(values)) + args[0] = "hmset" + args[1] = key + args = appendArgs(args, values) + cmd := NewBoolCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd { + cmd := NewBoolCmd(ctx, "hsetnx", key, field, value) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HVals(ctx context.Context, key string) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "hvals", key) + _ = c(ctx, cmd) + return cmd +} + +//------------------------------------------------------------------------------ + +func (c cmdable) BLPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd { + args := make([]interface{}, 1+len(keys)+1) + args[0] = "blpop" + for i, key := range keys { + args[1+i] = key + } + args[len(args)-1] = formatSec(ctx, timeout) + cmd := NewStringSliceCmd(ctx, args...) + cmd.setReadTimeout(timeout) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) BRPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd { + args := make([]interface{}, 1+len(keys)+1) + args[0] = "brpop" + for i, key := range keys { + args[1+i] = key + } + args[len(keys)+1] = formatSec(ctx, timeout) + cmd := NewStringSliceCmd(ctx, args...) + cmd.setReadTimeout(timeout) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) BRPopLPush(ctx context.Context, source, destination string, timeout time.Duration) *StringCmd { + cmd := NewStringCmd( + ctx, + "brpoplpush", + source, + destination, + formatSec(ctx, timeout), + ) + cmd.setReadTimeout(timeout) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) LIndex(ctx context.Context, key string, index int64) *StringCmd { + cmd := NewStringCmd(ctx, "lindex", key, index) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) LInsert(ctx context.Context, key, op string, pivot, value interface{}) *IntCmd { + cmd := NewIntCmd(ctx, "linsert", key, op, pivot, value) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) LInsertBefore(ctx context.Context, key string, pivot, value interface{}) *IntCmd { + cmd := NewIntCmd(ctx, "linsert", key, "before", pivot, value) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) LInsertAfter(ctx context.Context, key string, pivot, value interface{}) *IntCmd { + cmd := NewIntCmd(ctx, "linsert", key, "after", pivot, value) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) LLen(ctx context.Context, key string) *IntCmd { + cmd := NewIntCmd(ctx, "llen", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) LPop(ctx context.Context, key string) *StringCmd { + cmd := NewStringCmd(ctx, "lpop", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) LPush(ctx context.Context, key string, values ...interface{}) *IntCmd { + args := make([]interface{}, 2, 2+len(values)) + args[0] = "lpush" + args[1] = key + args = appendArgs(args, values) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) LPushX(ctx context.Context, key string, values ...interface{}) *IntCmd { + args := make([]interface{}, 2, 2+len(values)) + args[0] = "lpushx" + args[1] = key + args = appendArgs(args, values) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) LRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd { + cmd := NewStringSliceCmd( + ctx, + "lrange", + key, + start, + stop, + ) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) LRem(ctx context.Context, key string, count int64, value interface{}) *IntCmd { + cmd := NewIntCmd(ctx, "lrem", key, count, value) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) LSet(ctx context.Context, key string, index int64, value interface{}) *StatusCmd { + cmd := NewStatusCmd(ctx, "lset", key, index, value) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) LTrim(ctx context.Context, key string, start, stop int64) *StatusCmd { + cmd := NewStatusCmd( + ctx, + "ltrim", + key, + start, + stop, + ) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) RPop(ctx context.Context, key string) *StringCmd { + cmd := NewStringCmd(ctx, "rpop", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) RPopLPush(ctx context.Context, source, destination string) *StringCmd { + cmd := NewStringCmd(ctx, "rpoplpush", source, destination) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) RPush(ctx context.Context, key string, values ...interface{}) *IntCmd { + args := make([]interface{}, 2, 2+len(values)) + args[0] = "rpush" + args[1] = key + args = appendArgs(args, values) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) RPushX(ctx context.Context, key string, values ...interface{}) *IntCmd { + args := make([]interface{}, 2, 2+len(values)) + args[0] = "rpushx" + args[1] = key + args = appendArgs(args, values) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +//------------------------------------------------------------------------------ + +func (c cmdable) SAdd(ctx context.Context, key string, members ...interface{}) *IntCmd { + args := make([]interface{}, 2, 2+len(members)) + args[0] = "sadd" + args[1] = key + args = appendArgs(args, members) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SCard(ctx context.Context, key string) *IntCmd { + cmd := NewIntCmd(ctx, "scard", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SDiff(ctx context.Context, keys ...string) *StringSliceCmd { + args := make([]interface{}, 1+len(keys)) + args[0] = "sdiff" + for i, key := range keys { + args[1+i] = key + } + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SDiffStore(ctx context.Context, destination string, keys ...string) *IntCmd { + args := make([]interface{}, 2+len(keys)) + args[0] = "sdiffstore" + args[1] = destination + for i, key := range keys { + args[2+i] = key + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SInter(ctx context.Context, keys ...string) *StringSliceCmd { + args := make([]interface{}, 1+len(keys)) + args[0] = "sinter" + for i, key := range keys { + args[1+i] = key + } + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SInterStore(ctx context.Context, destination string, keys ...string) *IntCmd { + args := make([]interface{}, 2+len(keys)) + args[0] = "sinterstore" + args[1] = destination + for i, key := range keys { + args[2+i] = key + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SIsMember(ctx context.Context, key string, member interface{}) *BoolCmd { + cmd := NewBoolCmd(ctx, "sismember", key, member) + _ = c(ctx, cmd) + return cmd +} + +// Redis `SMEMBERS key` command output as a slice. +func (c cmdable) SMembers(ctx context.Context, key string) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "smembers", key) + _ = c(ctx, cmd) + return cmd +} + +// Redis `SMEMBERS key` command output as a map. +func (c cmdable) SMembersMap(ctx context.Context, key string) *StringStructMapCmd { + cmd := NewStringStructMapCmd(ctx, "smembers", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SMove(ctx context.Context, source, destination string, member interface{}) *BoolCmd { + cmd := NewBoolCmd(ctx, "smove", source, destination, member) + _ = c(ctx, cmd) + return cmd +} + +// Redis `SPOP key` command. +func (c cmdable) SPop(ctx context.Context, key string) *StringCmd { + cmd := NewStringCmd(ctx, "spop", key) + _ = c(ctx, cmd) + return cmd +} + +// Redis `SPOP key count` command. +func (c cmdable) SPopN(ctx context.Context, key string, count int64) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "spop", key, count) + _ = c(ctx, cmd) + return cmd +} + +// Redis `SRANDMEMBER key` command. +func (c cmdable) SRandMember(ctx context.Context, key string) *StringCmd { + cmd := NewStringCmd(ctx, "srandmember", key) + _ = c(ctx, cmd) + return cmd +} + +// Redis `SRANDMEMBER key count` command. +func (c cmdable) SRandMemberN(ctx context.Context, key string, count int64) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "srandmember", key, count) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SRem(ctx context.Context, key string, members ...interface{}) *IntCmd { + args := make([]interface{}, 2, 2+len(members)) + args[0] = "srem" + args[1] = key + args = appendArgs(args, members) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SUnion(ctx context.Context, keys ...string) *StringSliceCmd { + args := make([]interface{}, 1+len(keys)) + args[0] = "sunion" + for i, key := range keys { + args[1+i] = key + } + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) SUnionStore(ctx context.Context, destination string, keys ...string) *IntCmd { + args := make([]interface{}, 2+len(keys)) + args[0] = "sunionstore" + args[1] = destination + for i, key := range keys { + args[2+i] = key + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +//------------------------------------------------------------------------------ + +// XAddArgs accepts values in the following formats: +// - XAddArgs.Values = []interface{}{"key1", "value1", "key2", "value2"} +// - XAddArgs.Values = []string("key1", "value1", "key2", "value2") +// - XAddArgs.Values = map[string]interface{}{"key1": "value1", "key2": "value2"} +// +// Note that map will not preserve the order of key-value pairs. +type XAddArgs struct { + Stream string + MaxLen int64 // MAXLEN N + MaxLenApprox int64 // MAXLEN ~ N + ID string + Values interface{} +} + +func (c cmdable) XAdd(ctx context.Context, a *XAddArgs) *StringCmd { + args := make([]interface{}, 0, 8) + args = append(args, "xadd") + args = append(args, a.Stream) + if a.MaxLen > 0 { + args = append(args, "maxlen", a.MaxLen) + } else if a.MaxLenApprox > 0 { + args = append(args, "maxlen", "~", a.MaxLenApprox) + } + if a.ID != "" { + args = append(args, a.ID) + } else { + args = append(args, "*") + } + args = appendArg(args, a.Values) + + cmd := NewStringCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XDel(ctx context.Context, stream string, ids ...string) *IntCmd { + args := []interface{}{"xdel", stream} + for _, id := range ids { + args = append(args, id) + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XLen(ctx context.Context, stream string) *IntCmd { + cmd := NewIntCmd(ctx, "xlen", stream) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd { + cmd := NewXMessageSliceCmd(ctx, "xrange", stream, start, stop) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd { + cmd := NewXMessageSliceCmd(ctx, "xrange", stream, start, stop, "count", count) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XRevRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd { + cmd := NewXMessageSliceCmd(ctx, "xrevrange", stream, start, stop) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XRevRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd { + cmd := NewXMessageSliceCmd(ctx, "xrevrange", stream, start, stop, "count", count) + _ = c(ctx, cmd) + return cmd +} + +type XReadArgs struct { + Streams []string // list of streams and ids, e.g. stream1 stream2 id1 id2 + Count int64 + Block time.Duration +} + +func (c cmdable) XRead(ctx context.Context, a *XReadArgs) *XStreamSliceCmd { + args := make([]interface{}, 0, 5+len(a.Streams)) + args = append(args, "xread") + if a.Count > 0 { + args = append(args, "count") + args = append(args, a.Count) + } + if a.Block >= 0 { + args = append(args, "block") + args = append(args, int64(a.Block/time.Millisecond)) + } + + args = append(args, "streams") + for _, s := range a.Streams { + args = append(args, s) + } + + cmd := NewXStreamSliceCmd(ctx, args...) + if a.Block >= 0 { + cmd.setReadTimeout(a.Block) + } + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XReadStreams(ctx context.Context, streams ...string) *XStreamSliceCmd { + return c.XRead(ctx, &XReadArgs{ + Streams: streams, + Block: -1, + }) +} + +func (c cmdable) XGroupCreate(ctx context.Context, stream, group, start string) *StatusCmd { + cmd := NewStatusCmd(ctx, "xgroup", "create", stream, group, start) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XGroupCreateMkStream(ctx context.Context, stream, group, start string) *StatusCmd { + cmd := NewStatusCmd(ctx, "xgroup", "create", stream, group, start, "mkstream") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XGroupSetID(ctx context.Context, stream, group, start string) *StatusCmd { + cmd := NewStatusCmd(ctx, "xgroup", "setid", stream, group, start) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XGroupDestroy(ctx context.Context, stream, group string) *IntCmd { + cmd := NewIntCmd(ctx, "xgroup", "destroy", stream, group) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XGroupDelConsumer(ctx context.Context, stream, group, consumer string) *IntCmd { + cmd := NewIntCmd(ctx, "xgroup", "delconsumer", stream, group, consumer) + _ = c(ctx, cmd) + return cmd +} + +type XReadGroupArgs struct { + Group string + Consumer string + Streams []string // list of streams and ids, e.g. stream1 stream2 id1 id2 + Count int64 + Block time.Duration + NoAck bool +} + +func (c cmdable) XReadGroup(ctx context.Context, a *XReadGroupArgs) *XStreamSliceCmd { + args := make([]interface{}, 0, 8+len(a.Streams)) + args = append(args, "xreadgroup", "group", a.Group, a.Consumer) + if a.Count > 0 { + args = append(args, "count", a.Count) + } + if a.Block >= 0 { + args = append(args, "block", int64(a.Block/time.Millisecond)) + } + if a.NoAck { + args = append(args, "noack") + } + args = append(args, "streams") + for _, s := range a.Streams { + args = append(args, s) + } + + cmd := NewXStreamSliceCmd(ctx, args...) + if a.Block >= 0 { + cmd.setReadTimeout(a.Block) + } + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XAck(ctx context.Context, stream, group string, ids ...string) *IntCmd { + args := []interface{}{"xack", stream, group} + for _, id := range ids { + args = append(args, id) + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XPending(ctx context.Context, stream, group string) *XPendingCmd { + cmd := NewXPendingCmd(ctx, "xpending", stream, group) + _ = c(ctx, cmd) + return cmd +} + +type XPendingExtArgs struct { + Stream string + Group string + Start string + End string + Count int64 + Consumer string +} + +func (c cmdable) XPendingExt(ctx context.Context, a *XPendingExtArgs) *XPendingExtCmd { + args := make([]interface{}, 0, 7) + args = append(args, "xpending", a.Stream, a.Group, a.Start, a.End, a.Count) + if a.Consumer != "" { + args = append(args, a.Consumer) + } + cmd := NewXPendingExtCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +type XClaimArgs struct { + Stream string + Group string + Consumer string + MinIdle time.Duration + Messages []string +} + +func (c cmdable) XClaim(ctx context.Context, a *XClaimArgs) *XMessageSliceCmd { + args := xClaimArgs(a) + cmd := NewXMessageSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XClaimJustID(ctx context.Context, a *XClaimArgs) *StringSliceCmd { + args := xClaimArgs(a) + args = append(args, "justid") + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func xClaimArgs(a *XClaimArgs) []interface{} { + args := make([]interface{}, 0, 4+len(a.Messages)) + args = append(args, + "xclaim", + a.Stream, + a.Group, a.Consumer, + int64(a.MinIdle/time.Millisecond)) + for _, id := range a.Messages { + args = append(args, id) + } + return args +} + +func (c cmdable) XTrim(ctx context.Context, key string, maxLen int64) *IntCmd { + cmd := NewIntCmd(ctx, "xtrim", key, "maxlen", maxLen) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XTrimApprox(ctx context.Context, key string, maxLen int64) *IntCmd { + cmd := NewIntCmd(ctx, "xtrim", key, "maxlen", "~", maxLen) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XInfoGroups(ctx context.Context, key string) *XInfoGroupsCmd { + cmd := NewXInfoGroupsCmd(ctx, key) + _ = c(ctx, cmd) + return cmd +} + +//------------------------------------------------------------------------------ + +// Z represents sorted set member. +type Z struct { + Score float64 + Member interface{} +} + +// ZWithKey represents sorted set member including the name of the key where it was popped. +type ZWithKey struct { + Z + Key string +} + +// ZStore is used as an arg to ZInterStore and ZUnionStore. +type ZStore struct { + Keys []string + Weights []float64 + // Can be SUM, MIN or MAX. + Aggregate string +} + +// Redis `BZPOPMAX key [key ...] timeout` command. +func (c cmdable) BZPopMax(ctx context.Context, timeout time.Duration, keys ...string) *ZWithKeyCmd { + args := make([]interface{}, 1+len(keys)+1) + args[0] = "bzpopmax" + for i, key := range keys { + args[1+i] = key + } + args[len(args)-1] = formatSec(ctx, timeout) + cmd := NewZWithKeyCmd(ctx, args...) + cmd.setReadTimeout(timeout) + _ = c(ctx, cmd) + return cmd +} + +// Redis `BZPOPMIN key [key ...] timeout` command. +func (c cmdable) BZPopMin(ctx context.Context, timeout time.Duration, keys ...string) *ZWithKeyCmd { + args := make([]interface{}, 1+len(keys)+1) + args[0] = "bzpopmin" + for i, key := range keys { + args[1+i] = key + } + args[len(args)-1] = formatSec(ctx, timeout) + cmd := NewZWithKeyCmd(ctx, args...) + cmd.setReadTimeout(timeout) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) zAdd(ctx context.Context, a []interface{}, n int, members ...*Z) *IntCmd { + for i, m := range members { + a[n+2*i] = m.Score + a[n+2*i+1] = m.Member + } + cmd := NewIntCmd(ctx, a...) + _ = c(ctx, cmd) + return cmd +} + +// Redis `ZADD key score member [score member ...]` command. +func (c cmdable) ZAdd(ctx context.Context, key string, members ...*Z) *IntCmd { + const n = 2 + a := make([]interface{}, n+2*len(members)) + a[0], a[1] = "zadd", key + return c.zAdd(ctx, a, n, members...) +} + +// Redis `ZADD key NX score member [score member ...]` command. +func (c cmdable) ZAddNX(ctx context.Context, key string, members ...*Z) *IntCmd { + const n = 3 + a := make([]interface{}, n+2*len(members)) + a[0], a[1], a[2] = "zadd", key, "nx" + return c.zAdd(ctx, a, n, members...) +} + +// Redis `ZADD key XX score member [score member ...]` command. +func (c cmdable) ZAddXX(ctx context.Context, key string, members ...*Z) *IntCmd { + const n = 3 + a := make([]interface{}, n+2*len(members)) + a[0], a[1], a[2] = "zadd", key, "xx" + return c.zAdd(ctx, a, n, members...) +} + +// Redis `ZADD key CH score member [score member ...]` command. +func (c cmdable) ZAddCh(ctx context.Context, key string, members ...*Z) *IntCmd { + const n = 3 + a := make([]interface{}, n+2*len(members)) + a[0], a[1], a[2] = "zadd", key, "ch" + return c.zAdd(ctx, a, n, members...) +} + +// Redis `ZADD key NX CH score member [score member ...]` command. +func (c cmdable) ZAddNXCh(ctx context.Context, key string, members ...*Z) *IntCmd { + const n = 4 + a := make([]interface{}, n+2*len(members)) + a[0], a[1], a[2], a[3] = "zadd", key, "nx", "ch" + return c.zAdd(ctx, a, n, members...) +} + +// Redis `ZADD key XX CH score member [score member ...]` command. +func (c cmdable) ZAddXXCh(ctx context.Context, key string, members ...*Z) *IntCmd { + const n = 4 + a := make([]interface{}, n+2*len(members)) + a[0], a[1], a[2], a[3] = "zadd", key, "xx", "ch" + return c.zAdd(ctx, a, n, members...) +} + +func (c cmdable) zIncr(ctx context.Context, a []interface{}, n int, members ...*Z) *FloatCmd { + for i, m := range members { + a[n+2*i] = m.Score + a[n+2*i+1] = m.Member + } + cmd := NewFloatCmd(ctx, a...) + _ = c(ctx, cmd) + return cmd +} + +// Redis `ZADD key INCR score member` command. +func (c cmdable) ZIncr(ctx context.Context, key string, member *Z) *FloatCmd { + const n = 3 + a := make([]interface{}, n+2) + a[0], a[1], a[2] = "zadd", key, "incr" + return c.zIncr(ctx, a, n, member) +} + +// Redis `ZADD key NX INCR score member` command. +func (c cmdable) ZIncrNX(ctx context.Context, key string, member *Z) *FloatCmd { + const n = 4 + a := make([]interface{}, n+2) + a[0], a[1], a[2], a[3] = "zadd", key, "incr", "nx" + return c.zIncr(ctx, a, n, member) +} + +// Redis `ZADD key XX INCR score member` command. +func (c cmdable) ZIncrXX(ctx context.Context, key string, member *Z) *FloatCmd { + const n = 4 + a := make([]interface{}, n+2) + a[0], a[1], a[2], a[3] = "zadd", key, "incr", "xx" + return c.zIncr(ctx, a, n, member) +} + +func (c cmdable) ZCard(ctx context.Context, key string) *IntCmd { + cmd := NewIntCmd(ctx, "zcard", key) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZCount(ctx context.Context, key, min, max string) *IntCmd { + cmd := NewIntCmd(ctx, "zcount", key, min, max) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZLexCount(ctx context.Context, key, min, max string) *IntCmd { + cmd := NewIntCmd(ctx, "zlexcount", key, min, max) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZIncrBy(ctx context.Context, key string, increment float64, member string) *FloatCmd { + cmd := NewFloatCmd(ctx, "zincrby", key, increment, member) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZInterStore(ctx context.Context, destination string, store *ZStore) *IntCmd { + args := make([]interface{}, 3+len(store.Keys)) + args[0] = "zinterstore" + args[1] = destination + args[2] = len(store.Keys) + for i, key := range store.Keys { + args[3+i] = key + } + if len(store.Weights) > 0 { + args = append(args, "weights") + for _, weight := range store.Weights { + args = append(args, weight) + } + } + if store.Aggregate != "" { + args = append(args, "aggregate", store.Aggregate) + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZPopMax(ctx context.Context, key string, count ...int64) *ZSliceCmd { + args := []interface{}{ + "zpopmax", + key, + } + + switch len(count) { + case 0: + break + case 1: + args = append(args, count[0]) + default: + panic("too many arguments") + } + + cmd := NewZSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZPopMin(ctx context.Context, key string, count ...int64) *ZSliceCmd { + args := []interface{}{ + "zpopmin", + key, + } + + switch len(count) { + case 0: + break + case 1: + args = append(args, count[0]) + default: + panic("too many arguments") + } + + cmd := NewZSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) zRange(ctx context.Context, key string, start, stop int64, withScores bool) *StringSliceCmd { + args := []interface{}{ + "zrange", + key, + start, + stop, + } + if withScores { + args = append(args, "withscores") + } + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd { + return c.zRange(ctx, key, start, stop, false) +} + +func (c cmdable) ZRangeWithScores(ctx context.Context, key string, start, stop int64) *ZSliceCmd { + cmd := NewZSliceCmd(ctx, "zrange", key, start, stop, "withscores") + _ = c(ctx, cmd) + return cmd +} + +type ZRangeBy struct { + Min, Max string + Offset, Count int64 +} + +func (c cmdable) zRangeBy(ctx context.Context, zcmd, key string, opt *ZRangeBy, withScores bool) *StringSliceCmd { + args := []interface{}{zcmd, key, opt.Min, opt.Max} + if withScores { + args = append(args, "withscores") + } + if opt.Offset != 0 || opt.Count != 0 { + args = append( + args, + "limit", + opt.Offset, + opt.Count, + ) + } + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd { + return c.zRangeBy(ctx, "zrangebyscore", key, opt, false) +} + +func (c cmdable) ZRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd { + return c.zRangeBy(ctx, "zrangebylex", key, opt, false) +} + +func (c cmdable) ZRangeByScoreWithScores(ctx context.Context, key string, opt *ZRangeBy) *ZSliceCmd { + args := []interface{}{"zrangebyscore", key, opt.Min, opt.Max, "withscores"} + if opt.Offset != 0 || opt.Count != 0 { + args = append( + args, + "limit", + opt.Offset, + opt.Count, + ) + } + cmd := NewZSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZRank(ctx context.Context, key, member string) *IntCmd { + cmd := NewIntCmd(ctx, "zrank", key, member) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZRem(ctx context.Context, key string, members ...interface{}) *IntCmd { + args := make([]interface{}, 2, 2+len(members)) + args[0] = "zrem" + args[1] = key + args = appendArgs(args, members) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZRemRangeByRank(ctx context.Context, key string, start, stop int64) *IntCmd { + cmd := NewIntCmd( + ctx, + "zremrangebyrank", + key, + start, + stop, + ) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZRemRangeByScore(ctx context.Context, key, min, max string) *IntCmd { + cmd := NewIntCmd(ctx, "zremrangebyscore", key, min, max) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZRemRangeByLex(ctx context.Context, key, min, max string) *IntCmd { + cmd := NewIntCmd(ctx, "zremrangebylex", key, min, max) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZRevRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "zrevrange", key, start, stop) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZRevRangeWithScores(ctx context.Context, key string, start, stop int64) *ZSliceCmd { + cmd := NewZSliceCmd(ctx, "zrevrange", key, start, stop, "withscores") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) zRevRangeBy(ctx context.Context, zcmd, key string, opt *ZRangeBy) *StringSliceCmd { + args := []interface{}{zcmd, key, opt.Max, opt.Min} + if opt.Offset != 0 || opt.Count != 0 { + args = append( + args, + "limit", + opt.Offset, + opt.Count, + ) + } + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZRevRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd { + return c.zRevRangeBy(ctx, "zrevrangebyscore", key, opt) +} + +func (c cmdable) ZRevRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd { + return c.zRevRangeBy(ctx, "zrevrangebylex", key, opt) +} + +func (c cmdable) ZRevRangeByScoreWithScores(ctx context.Context, key string, opt *ZRangeBy) *ZSliceCmd { + args := []interface{}{"zrevrangebyscore", key, opt.Max, opt.Min, "withscores"} + if opt.Offset != 0 || opt.Count != 0 { + args = append( + args, + "limit", + opt.Offset, + opt.Count, + ) + } + cmd := NewZSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZRevRank(ctx context.Context, key, member string) *IntCmd { + cmd := NewIntCmd(ctx, "zrevrank", key, member) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZScore(ctx context.Context, key, member string) *FloatCmd { + cmd := NewFloatCmd(ctx, "zscore", key, member) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ZUnionStore(ctx context.Context, dest string, store *ZStore) *IntCmd { + args := make([]interface{}, 3+len(store.Keys)) + args[0] = "zunionstore" + args[1] = dest + args[2] = len(store.Keys) + for i, key := range store.Keys { + args[3+i] = key + } + if len(store.Weights) > 0 { + args = append(args, "weights") + for _, weight := range store.Weights { + args = append(args, weight) + } + } + if store.Aggregate != "" { + args = append(args, "aggregate", store.Aggregate) + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +//------------------------------------------------------------------------------ + +func (c cmdable) PFAdd(ctx context.Context, key string, els ...interface{}) *IntCmd { + args := make([]interface{}, 2, 2+len(els)) + args[0] = "pfadd" + args[1] = key + args = appendArgs(args, els) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) PFCount(ctx context.Context, keys ...string) *IntCmd { + args := make([]interface{}, 1+len(keys)) + args[0] = "pfcount" + for i, key := range keys { + args[1+i] = key + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) PFMerge(ctx context.Context, dest string, keys ...string) *StatusCmd { + args := make([]interface{}, 2+len(keys)) + args[0] = "pfmerge" + args[1] = dest + for i, key := range keys { + args[2+i] = key + } + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +//------------------------------------------------------------------------------ + +func (c cmdable) BgRewriteAOF(ctx context.Context) *StatusCmd { + cmd := NewStatusCmd(ctx, "bgrewriteaof") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) BgSave(ctx context.Context) *StatusCmd { + cmd := NewStatusCmd(ctx, "bgsave") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ClientKill(ctx context.Context, ipPort string) *StatusCmd { + cmd := NewStatusCmd(ctx, "client", "kill", ipPort) + _ = c(ctx, cmd) + return cmd +} + +// ClientKillByFilter is new style syntax, while the ClientKill is old +// +// CLIENT KILL