From 421d0828c9a69ffc36adbf1e87919f82f71e46d1 Mon Sep 17 00:00:00 2001 From: Bartlomiej Plotka Date: Fri, 29 Jan 2021 20:45:18 +0000 Subject: [PATCH 1/2] Ported Cortex e2e modules, so we can reuse it smaller projects too. Signed-off-by: Bartlomiej Plotka --- Makefile | 3 +- README.md | 24 +- core/go.mod | 1 - core/pkg/backoff/backoff.go | 109 +++++ core/pkg/backoff/backoff_test.go | 105 +++++ core/pkg/merrors/errors_test.go | 174 ++++---- e2e/cli.go | 62 +++ e2e/composite_service.go | 91 +++++ e2e/db/db.go | 151 +++++++ e2e/doc.go | 11 + e2e/go.mod | 14 + e2e/go.sum | 459 +++++++++++++++++++++ e2e/internal/matchers/matcher.go | 117 ++++++ e2e/internal/matchers/regexp.go | 107 +++++ e2e/internal/s3/s3.go | 482 ++++++++++++++++++++++ e2e/metrics.go | 212 ++++++++++ e2e/scenario.go | 297 ++++++++++++++ e2e/scenario_test.go | 147 +++++++ e2e/service.go | 674 +++++++++++++++++++++++++++++++ e2e/service_test.go | 169 ++++++++ 20 files changed, 3317 insertions(+), 92 deletions(-) create mode 100644 core/pkg/backoff/backoff.go create mode 100644 core/pkg/backoff/backoff_test.go create mode 100644 e2e/cli.go create mode 100644 e2e/composite_service.go create mode 100644 e2e/db/db.go create mode 100644 e2e/doc.go create mode 100644 e2e/go.mod create mode 100644 e2e/go.sum create mode 100644 e2e/internal/matchers/matcher.go create mode 100644 e2e/internal/matchers/regexp.go create mode 100644 e2e/internal/s3/s3.go create mode 100644 e2e/metrics.go create mode 100644 e2e/scenario.go create mode 100644 e2e/scenario_test.go create mode 100644 e2e/service.go create mode 100644 e2e/service_test.go diff --git a/Makefile b/Makefile index 96e6f7d..b81e2ef 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,8 @@ lint: $(FAILLINT) $(GOLANGCI_LINT) $(MISSPELL) build format docs check-git deps $(call require_clean_work_tree,"detected not clean master before running lint - run make lint and commit changes.") @echo ">> verifying imported " for dir in $(MODULES) ; do \ - cd $${dir} && $(FAILLINT) -paths "fmt.{Print,PrintfPrintln,Sprint}" -ignore-tests ./...; \ + cd $${dir} && $(FAILLINT) -paths "fmt.{Print,PrintfPrintln,Sprint}" -ignore-tests ./...; && \ + $(FAILLINT) -paths "github.com/stretchr/testify=github.com/efficientgo/tools/core/pkg/testutil" ./...; \ done @echo ">> examining all of the Go files" for dir in $(MODULES) ; do \ diff --git a/README.md b/README.md index 81dd790..00ddfa5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![golang docs](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/efficientgo/tools) -Set of tools, packages and libraries that every open-source Go project always needs with almost no dependencies. +Set of lightweight tools, packages and modules that every open-source Go project always needs with almost no dependencies. ## Release model @@ -10,7 +10,7 @@ Since this is meant to be critical, tiny import, multi module toolset, there are ## Modules -### `github.com/efficientgo/tools/core` +### Module `github.com/efficientgo/tools/core` The main module containing set of useful, core packages for testing, closing, running and repeating. @@ -148,7 +148,25 @@ This module contains: // Simplistic assertion helpers for testing code. TestOrBench utils for union of testing and benchmarks. ``` -### `github.com/efficientgo/tools/copyright` +### Module `github.com/efficientgo/tools/e2e` + +This module is a fully featured e2e suite allowing utilizing `go test` for setting hermetic up complex microservice testing scenarios using docker. + +```go mdox-gen-exec="sh -c 'tail -n +6 e2e/doc.go'" +// This module is a fully featured e2e suite allowing utilizing `go test` for setting hermetic up complex microservice integration testing scenarios using docker. +// Example usages: +// * https://github.com/cortexproject/cortex/tree/master/integration +// * https://github.com/thanos-io/thanos/tree/master/test/e2e +// +// Check github.com/efficientgo/tools/e2e/db for common DBs services you can run out of the box. +``` + +Credits: + +* [Cortex Team](https://github.com/cortexproject/cortex/tree/f639b1855c9f0c9564113709a6bce2996d151ec7/integration) +* Initial Author: [@pracucci](https://github.com/pracucci) + +### Module `github.com/efficientgo/tools/copyright` This module is a very simple CLI for ensuring copyright header on code files. diff --git a/core/go.mod b/core/go.mod index 87eb9d2..f6c95fc 100644 --- a/core/go.mod +++ b/core/go.mod @@ -6,6 +6,5 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.0 - github.com/stretchr/testify v1.6.1 go.uber.org/goleak v1.1.10 ) diff --git a/core/pkg/backoff/backoff.go b/core/pkg/backoff/backoff.go new file mode 100644 index 0000000..f32105e --- /dev/null +++ b/core/pkg/backoff/backoff.go @@ -0,0 +1,109 @@ +package backoff + +// Copied from https://github.com/cortexproject/cortex/blob/0ec7b9664a01d538f1f49580b4c359a5c3cc755a/pkg/util/backoff.go + +import ( + "context" + "fmt" + "math/rand" + "time" +) + +// Config configures a Backoff. +type Config struct { + Min time.Duration `yaml:"min_period"` // Start backoff at this level + Max time.Duration `yaml:"max_period"` // Increase exponentially to this level + MaxRetries int `yaml:"max_retries"` // Give up after this many; zero means infinite retries +} + +// Backoff implements exponential backoff with randomized wait times. +type Backoff struct { + cfg Config + ctx context.Context + numRetries int + nextDelayMin time.Duration + nextDelayMax time.Duration +} + +// New creates a Backoff object. Pass a Context that can also terminate the operation. +func New(ctx context.Context, cfg Config) *Backoff { + return &Backoff{ + cfg: cfg, + ctx: ctx, + nextDelayMin: cfg.Min, + nextDelayMax: doubleDuration(cfg.Min, cfg.Max), + } +} + +// Reset the Backoff back to its initial condition. +func (b *Backoff) Reset() { + b.numRetries = 0 + b.nextDelayMin = b.cfg.Min + b.nextDelayMax = doubleDuration(b.cfg.Min, b.cfg.Max) +} + +// Ongoing returns true if caller should keep going. +func (b *Backoff) Ongoing() bool { + // Stop if Context has errored or max retry count is exceeded. + return b.ctx.Err() == nil && (b.cfg.MaxRetries == 0 || b.numRetries < b.cfg.MaxRetries) +} + +// Err returns the reason for terminating the backoff, or nil if it didn't terminate. +func (b *Backoff) Err() error { + if b.ctx.Err() != nil { + return b.ctx.Err() + } + if b.cfg.MaxRetries != 0 && b.numRetries >= b.cfg.MaxRetries { + return fmt.Errorf("terminated after %d retries", b.numRetries) + } + return nil +} + +// NumRetries returns the number of retries so far. +func (b *Backoff) NumRetries() int { + return b.numRetries +} + +// Wait sleeps for the backoff time then increases the retry count and backoff time. +// Returns immediately if Context is terminated. +func (b *Backoff) Wait() { + // Increase the number of retries and get the next delay. + sleepTime := b.NextDelay() + + if b.Ongoing() { + select { + case <-b.ctx.Done(): + case <-time.After(sleepTime): + } + } +} + +func (b *Backoff) NextDelay() time.Duration { + b.numRetries++ + + // Handle the edge case the min and max have the same value + // (or due to some misconfig max is < min). + if b.nextDelayMin >= b.nextDelayMax { + return b.nextDelayMin + } + + // Add a jitter within the next exponential backoff range. + sleepTime := b.nextDelayMin + time.Duration(rand.Int63n(int64(b.nextDelayMax-b.nextDelayMin))) + + // Apply the exponential backoff to calculate the next jitter + // range, unless we've already reached the max. + if b.nextDelayMax < b.cfg.Max { + b.nextDelayMin = doubleDuration(b.nextDelayMin, b.cfg.Max) + b.nextDelayMax = doubleDuration(b.nextDelayMax, b.cfg.Max) + } + + return sleepTime +} + +func doubleDuration(value time.Duration, max time.Duration) time.Duration { + value = value * 2 + if value <= max { + return value + } + return max +} diff --git a/core/pkg/backoff/backoff_test.go b/core/pkg/backoff/backoff_test.go new file mode 100644 index 0000000..c209672 --- /dev/null +++ b/core/pkg/backoff/backoff_test.go @@ -0,0 +1,105 @@ +package backoff + +// Copied from https://github.com/cortexproject/cortex/blob/0ec7b9664a01d538f1f49580b4c359a5c3cc755a/pkg/util/backoff.go + +import ( + "context" + "testing" + "time" +) + +func TestBackoff_NextDelay(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + minBackoff time.Duration + maxBackoff time.Duration + expectedRanges [][]time.Duration + }{ + "exponential backoff with jitter honoring min and max": { + minBackoff: 100 * time.Millisecond, + maxBackoff: 10 * time.Second, + expectedRanges: [][]time.Duration{ + {100 * time.Millisecond, 200 * time.Millisecond}, + {200 * time.Millisecond, 400 * time.Millisecond}, + {400 * time.Millisecond, 800 * time.Millisecond}, + {800 * time.Millisecond, 1600 * time.Millisecond}, + {1600 * time.Millisecond, 3200 * time.Millisecond}, + {3200 * time.Millisecond, 6400 * time.Millisecond}, + {6400 * time.Millisecond, 10000 * time.Millisecond}, + {6400 * time.Millisecond, 10000 * time.Millisecond}, + }, + }, + "exponential backoff with max equal to the end of a range": { + minBackoff: 100 * time.Millisecond, + maxBackoff: 800 * time.Millisecond, + expectedRanges: [][]time.Duration{ + {100 * time.Millisecond, 200 * time.Millisecond}, + {200 * time.Millisecond, 400 * time.Millisecond}, + {400 * time.Millisecond, 800 * time.Millisecond}, + {400 * time.Millisecond, 800 * time.Millisecond}, + }, + }, + "exponential backoff with max equal to the end of a range + 1": { + minBackoff: 100 * time.Millisecond, + maxBackoff: 801 * time.Millisecond, + expectedRanges: [][]time.Duration{ + {100 * time.Millisecond, 200 * time.Millisecond}, + {200 * time.Millisecond, 400 * time.Millisecond}, + {400 * time.Millisecond, 800 * time.Millisecond}, + {800 * time.Millisecond, 801 * time.Millisecond}, + {800 * time.Millisecond, 801 * time.Millisecond}, + }, + }, + "exponential backoff with max equal to the end of a range - 1": { + minBackoff: 100 * time.Millisecond, + maxBackoff: 799 * time.Millisecond, + expectedRanges: [][]time.Duration{ + {100 * time.Millisecond, 200 * time.Millisecond}, + {200 * time.Millisecond, 400 * time.Millisecond}, + {400 * time.Millisecond, 799 * time.Millisecond}, + {400 * time.Millisecond, 799 * time.Millisecond}, + }, + }, + "min backoff is equal to max": { + minBackoff: 100 * time.Millisecond, + maxBackoff: 100 * time.Millisecond, + expectedRanges: [][]time.Duration{ + {100 * time.Millisecond, 100 * time.Millisecond}, + {100 * time.Millisecond, 100 * time.Millisecond}, + {100 * time.Millisecond, 100 * time.Millisecond}, + }, + }, + "min backoff is greater then max": { + minBackoff: 200 * time.Millisecond, + maxBackoff: 100 * time.Millisecond, + expectedRanges: [][]time.Duration{ + {200 * time.Millisecond, 200 * time.Millisecond}, + {200 * time.Millisecond, 200 * time.Millisecond}, + {200 * time.Millisecond, 200 * time.Millisecond}, + }, + }, + } + + for testName, testData := range tests { + testData := testData + + t.Run(testName, func(t *testing.T) { + t.Parallel() + + b := New(context.Background(), Config{ + Min: testData.minBackoff, + Max: testData.maxBackoff, + MaxRetries: len(testData.expectedRanges), + }) + + for _, expectedRange := range testData.expectedRanges { + delay := b.NextDelay() + + if delay < expectedRange[0] || delay > expectedRange[1] { + t.Errorf("%d expected to be within %d and %d", delay, expectedRange[0], expectedRange[1]) + } + } + }) + } +} diff --git a/core/pkg/merrors/errors_test.go b/core/pkg/merrors/errors_test.go index 315eab7..d9cef6e 100644 --- a/core/pkg/merrors/errors_test.go +++ b/core/pkg/merrors/errors_test.go @@ -8,57 +8,57 @@ import ( "testing" "github.com/efficientgo/tools/core/pkg/merrors" + "github.com/efficientgo/tools/core/pkg/testutil" pkgerrors "github.com/pkg/errors" - "github.com/stretchr/testify/require" ) func TestNilMultiError(t *testing.T) { - require.NoError(t, merrors.New().Err()) - require.NoError(t, merrors.New(nil, nil, nil).Err()) + testutil.Ok(t, merrors.New().Err()) + testutil.Ok(t, merrors.New(nil, nil, nil).Err()) e := merrors.New() e.Add() - require.NoError(t, e.Err()) + testutil.Ok(t, e.Err()) e = merrors.New(nil, nil, nil) e.Add() - require.NoError(t, e.Err()) + testutil.Ok(t, e.Err()) e = merrors.New() e.Add(nil, nil, nil) - require.NoError(t, e.Err()) + testutil.Ok(t, e.Err()) e = merrors.New(nil, nil, nil) e.Add(nil, nil, nil) - require.NoError(t, e.Err()) + testutil.Ok(t, e.Err()) } func TestMultiError(t *testing.T) { err := stderrors.New("test1") - require.Error(t, merrors.New(err).Err()) - require.Error(t, merrors.New(nil, err, nil).Err()) + testutil.NotOk(t, merrors.New(err).Err()) + testutil.NotOk(t, merrors.New(nil, err, nil).Err()) e := merrors.New(err) e.Add() - require.Error(t, e.Err()) + testutil.NotOk(t, e.Err()) e = merrors.New(nil, nil, nil) e.Add(err) - require.Error(t, e.Err()) + testutil.NotOk(t, e.Err()) e = merrors.New(err) e.Add(nil, nil, nil) - require.Error(t, e.Err()) + testutil.NotOk(t, e.Err()) e = merrors.New(nil, nil, nil) e.Add(nil, err, nil) - require.Error(t, e.Err()) + testutil.NotOk(t, e.Err()) - require.Error(t, func() error { + testutil.NotOk(t, func() error { return e.Err() }()) - require.NoError(t, func() error { + testutil.Ok(t, func() error { return merrors.New(nil, nil, nil).Err() }()) } @@ -66,9 +66,9 @@ func TestMultiError(t *testing.T) { func TestMultiError_Error(t *testing.T) { err := stderrors.New("test1") - require.Equal(t, "test1", merrors.New(err).Err().Error()) - require.Equal(t, "test1", merrors.New(err, nil).Err().Error()) - require.Equal(t, "4 errors: test1; test1; test2; test3", merrors.New(err, err, stderrors.New("test2"), nil, stderrors.New("test3")).Err().Error()) + testutil.Equals(t, "test1", merrors.New(err).Err().Error()) + testutil.Equals(t, "test1", merrors.New(err, nil).Err().Error()) + testutil.Equals(t, "4 errors: test1; test1; test2; test3", merrors.New(err, err, stderrors.New("test2"), nil, stderrors.New("test3")).Err().Error()) } type customErr struct{ error } @@ -80,91 +80,91 @@ type customErr3 struct{ error } func TestMultiError_As(t *testing.T) { err := customErr{error: stderrors.New("err1")} - require.True(t, stderrors.As(err, &err)) - require.True(t, stderrors.As(err, &customErr{})) + testutil.Assert(t, stderrors.As(err, &err)) + testutil.Assert(t, stderrors.As(err, &customErr{})) - require.False(t, stderrors.As(err, &customErr2{})) - require.False(t, stderrors.As(err, &customErr3{})) + testutil.Assert(t, !stderrors.As(err, &customErr2{})) + testutil.Assert(t, !stderrors.As(err, &customErr3{})) // This is just to show limitation of std As. - require.False(t, stderrors.As(&err, &err)) - require.False(t, stderrors.As(&err, &customErr{})) - require.False(t, stderrors.As(&err, &customErr2{})) - require.False(t, stderrors.As(&err, &customErr3{})) + testutil.Assert(t, !stderrors.As(&err, &err)) + testutil.Assert(t, !stderrors.As(&err, &customErr{})) + testutil.Assert(t, !stderrors.As(&err, &customErr2{})) + testutil.Assert(t, !stderrors.As(&err, &customErr3{})) e := merrors.New(err).Err() - require.True(t, stderrors.As(e, &customErr{})) + testutil.Assert(t, stderrors.As(e, &customErr{})) same := merrors.New(err).Err() - require.True(t, stderrors.As(e, &same)) - require.False(t, stderrors.As(e, &customErr2{})) - require.False(t, stderrors.As(e, &customErr3{})) + testutil.Assert(t, stderrors.As(e, &same)) + testutil.Assert(t, !stderrors.As(e, &customErr2{})) + testutil.Assert(t, !stderrors.As(e, &customErr3{})) e2 := merrors.New(err, customErr3{error: stderrors.New("some")}).Err() - require.True(t, stderrors.As(e2, &customErr{})) - require.True(t, stderrors.As(e2, &customErr3{})) - require.False(t, stderrors.As(e2, &customErr2{})) + testutil.Assert(t, stderrors.As(e2, &customErr{})) + testutil.Assert(t, stderrors.As(e2, &customErr3{})) + testutil.Assert(t, !stderrors.As(e2, &customErr2{})) // Wrapped. e3 := pkgerrors.Wrap(merrors.New(err, customErr3{}).Err(), "wrap") - require.True(t, stderrors.As(e3, &customErr{})) - require.True(t, stderrors.As(e3, &customErr3{})) - require.False(t, stderrors.As(e3, &customErr2{})) + testutil.Assert(t, stderrors.As(e3, &customErr{})) + testutil.Assert(t, stderrors.As(e3, &customErr3{})) + testutil.Assert(t, !stderrors.As(e3, &customErr2{})) // This is just to show limitation of std As. e4 := merrors.New(err, &customErr3{}).Err() - require.False(t, stderrors.As(e4, &customErr2{})) - require.False(t, stderrors.As(e4, &customErr3{})) + testutil.Assert(t, !stderrors.As(e4, &customErr2{})) + testutil.Assert(t, !stderrors.As(e4, &customErr3{})) } func TestMultiError_Is(t *testing.T) { err := customErr{error: stderrors.New("err1")} - require.True(t, stderrors.Is(err, err)) - require.True(t, stderrors.Is(err, customErr{error: err.error})) - require.False(t, stderrors.Is(err, &err)) - require.False(t, stderrors.Is(err, customErr{})) - require.False(t, stderrors.Is(err, customErr{error: stderrors.New("err1")})) - require.False(t, stderrors.Is(err, customErr2{})) - require.False(t, stderrors.Is(err, customErr3{})) + testutil.Assert(t, stderrors.Is(err, err)) + testutil.Assert(t, stderrors.Is(err, customErr{error: err.error})) + testutil.Assert(t, !stderrors.Is(err, &err)) + testutil.Assert(t, !stderrors.Is(err, customErr{})) + testutil.Assert(t, !stderrors.Is(err, customErr{error: stderrors.New("err1")})) + testutil.Assert(t, !stderrors.Is(err, customErr2{})) + testutil.Assert(t, !stderrors.Is(err, customErr3{})) - require.True(t, stderrors.Is(&err, &err)) - require.False(t, stderrors.Is(&err, &customErr{error: err.error})) - require.False(t, stderrors.Is(&err, &customErr2{})) - require.False(t, stderrors.Is(&err, &customErr3{})) + testutil.Assert(t, stderrors.Is(&err, &err)) + testutil.Assert(t, !stderrors.Is(&err, &customErr{error: err.error})) + testutil.Assert(t, !stderrors.Is(&err, &customErr2{})) + testutil.Assert(t, !stderrors.Is(&err, &customErr3{})) e := merrors.New(err).Err() - require.True(t, stderrors.Is(e, err)) - require.True(t, stderrors.Is(err, customErr{error: err.error})) - require.True(t, stderrors.Is(e, e)) - require.True(t, stderrors.Is(e, merrors.New(err).Err())) - require.False(t, stderrors.Is(e, &err)) - require.False(t, stderrors.Is(err, customErr{})) - require.False(t, stderrors.Is(e, customErr2{})) - require.False(t, stderrors.Is(e, customErr3{})) + testutil.Assert(t, stderrors.Is(e, err)) + testutil.Assert(t, stderrors.Is(err, customErr{error: err.error})) + testutil.Assert(t, stderrors.Is(e, e)) + testutil.Assert(t, stderrors.Is(e, merrors.New(err).Err())) + testutil.Assert(t, !stderrors.Is(e, &err)) + testutil.Assert(t, !stderrors.Is(err, customErr{})) + testutil.Assert(t, !stderrors.Is(e, customErr2{})) + testutil.Assert(t, !stderrors.Is(e, customErr3{})) e2 := merrors.New(err, customErr3{}).Err() - require.True(t, stderrors.Is(e2, err)) - require.True(t, stderrors.Is(e2, customErr3{})) - require.True(t, stderrors.Is(e2, merrors.New(err, customErr3{}).Err())) - require.False(t, stderrors.Is(e2, merrors.New(customErr3{}, err).Err())) - require.False(t, stderrors.Is(e2, customErr{})) - require.False(t, stderrors.Is(e2, customErr2{})) + testutil.Assert(t, stderrors.Is(e2, err)) + testutil.Assert(t, stderrors.Is(e2, customErr3{})) + testutil.Assert(t, stderrors.Is(e2, merrors.New(err, customErr3{}).Err())) + testutil.Assert(t, !stderrors.Is(e2, merrors.New(customErr3{}, err).Err())) + testutil.Assert(t, !stderrors.Is(e2, customErr{})) + testutil.Assert(t, !stderrors.Is(e2, customErr2{})) // Wrapped. e3 := pkgerrors.Wrap(merrors.New(err, customErr3{}).Err(), "wrap") - require.True(t, stderrors.Is(e3, err)) - require.True(t, stderrors.Is(e3, customErr3{})) - require.False(t, stderrors.Is(e3, customErr{})) - require.False(t, stderrors.Is(e3, customErr2{})) + testutil.Assert(t, stderrors.Is(e3, err)) + testutil.Assert(t, stderrors.Is(e3, customErr3{})) + testutil.Assert(t, !stderrors.Is(e3, customErr{})) + testutil.Assert(t, !stderrors.Is(e3, customErr2{})) exact := &customErr3{} e4 := merrors.New(err, exact).Err() - require.True(t, stderrors.Is(e4, err)) - require.True(t, stderrors.Is(e4, exact)) - require.True(t, stderrors.Is(e4, merrors.New(err, exact).Err())) - require.False(t, stderrors.Is(e4, customErr{})) - require.False(t, stderrors.Is(e4, customErr2{})) - require.False(t, stderrors.Is(e4, &customErr3{})) + testutil.Assert(t, stderrors.Is(e4, err)) + testutil.Assert(t, stderrors.Is(e4, exact)) + testutil.Assert(t, stderrors.Is(e4, merrors.New(err, exact).Err())) + testutil.Assert(t, !stderrors.Is(e4, customErr{})) + testutil.Assert(t, !stderrors.Is(e4, customErr2{})) + testutil.Assert(t, !stderrors.Is(e4, &customErr3{})) } func TestMultiError_Count(t *testing.T) { @@ -173,17 +173,17 @@ func TestMultiError_Count(t *testing.T) { merr.Add(customErr3{}) m, ok := merrors.AsMulti(merr.Err()) - require.True(t, ok) - require.Equal(t, 0, m.Count(err)) - require.Equal(t, 1, m.Count(customErr3{})) + testutil.Assert(t, ok) + testutil.Equals(t, 0, m.Count(err)) + testutil.Equals(t, 1, m.Count(customErr3{})) merr.Add(customErr3{}) merr.Add(customErr3{}) m, ok = merrors.AsMulti(merr.Err()) - require.True(t, ok) - require.Equal(t, 0, m.Count(err)) - require.Equal(t, 3, m.Count(customErr3{})) + testutil.Assert(t, ok) + testutil.Equals(t, 0, m.Count(err)) + testutil.Equals(t, 3, m.Count(customErr3{})) // Nest multi errors with wraps. merr2 := merrors.New() @@ -200,9 +200,9 @@ func TestMultiError_Count(t *testing.T) { merr.Add(pkgerrors.Wrap(merr2.Err(), "wrap")) m, ok = merrors.AsMulti(merr.Err()) - require.True(t, ok) - require.Equal(t, 0, m.Count(err)) - require.Equal(t, 8, m.Count(customErr3{})) + testutil.Assert(t, ok) + testutil.Equals(t, 0, m.Count(err)) + testutil.Equals(t, 8, m.Count(customErr3{})) } func TestAsMulti(t *testing.T) { @@ -211,13 +211,13 @@ func TestAsMulti(t *testing.T) { wrapped := pkgerrors.Wrap(merr, "wrap") _, ok := merrors.AsMulti(err) - require.False(t, ok) + testutil.Assert(t, !ok) m, ok := merrors.AsMulti(merr) - require.True(t, ok) - require.True(t, stderrors.Is(m, merr)) + testutil.Assert(t, ok) + testutil.Assert(t, stderrors.Is(m, merr)) m, ok = merrors.AsMulti(wrapped) - require.True(t, ok) - require.True(t, stderrors.Is(m, merr)) + testutil.Assert(t, ok) + testutil.Assert(t, stderrors.Is(m, merr)) } diff --git a/e2e/cli.go b/e2e/cli.go new file mode 100644 index 0000000..b999461 --- /dev/null +++ b/e2e/cli.go @@ -0,0 +1,62 @@ +package e2e + +import ( + "context" + "os/exec" + "time" +) + +func RunCommandAndGetOutput(name string, args ...string) ([]byte, error) { + cmd := exec.Command(name, args...) + return cmd.CombinedOutput() +} + +func RunCommandWithTimeoutAndGetOutput(timeout time.Duration, name string, args ...string) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + cmd := exec.CommandContext(ctx, name, args...) + return cmd.CombinedOutput() +} + +func EmptyFlags() map[string]string { + return map[string]string{} +} + +func MergeFlags(inputs ...map[string]string) map[string]string { + output := MergeFlagsWithoutRemovingEmpty(inputs...) + + for k, v := range output { + if v == "" { + delete(output, k) + } + } + + return output +} + +func MergeFlagsWithoutRemovingEmpty(inputs ...map[string]string) map[string]string { + output := map[string]string{} + + for _, input := range inputs { + for name, value := range input { + output[name] = value + } + } + + return output +} + +func BuildArgs(flags map[string]string) []string { + args := make([]string, 0, len(flags)) + + for name, value := range flags { + if value != "" { + args = append(args, name+"="+value) + } else { + args = append(args, name) + } + } + + return args +} diff --git a/e2e/composite_service.go b/e2e/composite_service.go new file mode 100644 index 0000000..1165d82 --- /dev/null +++ b/e2e/composite_service.go @@ -0,0 +1,91 @@ +package e2e + +import ( + "context" + "fmt" + "time" + + "github.com/efficientgo/tools/core/pkg/backoff" + "github.com/pkg/errors" +) + +// CompositeHTTPService abstract an higher-level service composed by more than one HTTPService. +type CompositeHTTPService struct { + services []*HTTPService + + // Generic retry backoff. + backoff *backoff.Backoff +} + +func NewCompositeHTTPService(services ...*HTTPService) *CompositeHTTPService { + return &CompositeHTTPService{ + services: services, + backoff: backoff.New(context.Background(), backoff.Config{ + Min: 300 * time.Millisecond, + Max: 600 * time.Millisecond, + MaxRetries: 50, // Sometimes the CI is slow ¯\_(ツ)_/¯ + }), + } +} + +func (s *CompositeHTTPService) NumInstances() int { + return len(s.services) +} + +func (s *CompositeHTTPService) Instances() []*HTTPService { + return s.services +} + +// WaitSumMetrics waits for at least one instance of each given metric names to be present and their sums, returning true +// when passed to given isExpected(...). +func (s *CompositeHTTPService) WaitSumMetrics(isExpected func(sums ...float64) bool, metricNames ...string) error { + return s.WaitSumMetricsWithOptions(isExpected, metricNames) +} + +func (s *CompositeHTTPService) WaitSumMetricsWithOptions(isExpected func(sums ...float64) bool, metricNames []string, opts ...MetricsOption) error { + var ( + sums []float64 + err error + options = buildMetricsOptions(opts) + ) + + for s.backoff.Reset(); s.backoff.Ongoing(); { + sums, err = s.SumMetrics(metricNames, opts...) + if options.waitMissingMetrics && errors.Is(err, errMissingMetric) { + continue + } + if err != nil { + return err + } + + if isExpected(sums...) { + return nil + } + + s.backoff.Wait() + } + + return fmt.Errorf("unable to find metrics %s with expected values. Last error: %v. Last values: %v", metricNames, err, sums) +} + +// SumMetrics returns the sum of the values of each given metric names. +func (s *CompositeHTTPService) SumMetrics(metricNames []string, opts ...MetricsOption) ([]float64, error) { + sums := make([]float64, len(metricNames)) + + for _, service := range s.services { + partials, err := service.SumMetrics(metricNames, opts...) + if err != nil { + return nil, err + } + + if len(partials) != len(sums) { + return nil, fmt.Errorf("unexpected mismatching sum metrics results (got %d, expected %d)", len(partials), len(sums)) + } + + for i := 0; i < len(sums); i++ { + sums[i] += partials[i] + } + } + + return sums, nil +} diff --git a/e2e/db/db.go b/e2e/db/db.go new file mode 100644 index 0000000..b6ffeed --- /dev/null +++ b/e2e/db/db.go @@ -0,0 +1,151 @@ +package e2edb + +import ( + "fmt" + "strings" + + "github.com/efficientgo/tools/e2e" +) + +const ( + MinioAccessKey = "Cheescake" + MinioSecretKey = "supersecret" +) + +type Builder struct { + MemcachedImage, MinioImage, ConsulImage, EtcdImage, DynamoDBImage, BigtableEmulatorImage, CassandraImage, SwiftEmulatorImage string + MemcachedHTTPPort, MinioHTTPPort, ConsulHTTPPort, EtcdHTTPPort, DynamoDBHTTPPort, BigtableEmulatorHTTPPort, CassandraHTTPPort, SwiftEmulatorHTTPPort int +} + +// Default returns default builder. Create your own if you want to adjust the images or ports. +func Default() Builder { + return Builder{ + MemcachedImage: "memcached:1.6.1", + MemcachedHTTPPort: 11211, + + MinioImage: "minio/minio:RELEASE.2019-12-30T05-45-39Z", + MinioHTTPPort: 8090, + + ConsulImage: "consul:1.8.4", + ConsulHTTPPort: 8500, + + EtcdImage: "gcr.io/etcd-development/etcd:v3.4.7", + EtcdHTTPPort: 2379, + + DynamoDBImage: "amazon/dynamodb-local:1.11.477", + DynamoDBHTTPPort: 8000, + + BigtableEmulatorImage: "shopify/bigtable-emulator:0.1.0", + BigtableEmulatorHTTPPort: 9035, + + CassandraImage: "rinscy/cassandra:3.11.0", + CassandraHTTPPort: 9042, + + SwiftEmulatorImage: "bouncestorage/swift-aio:55ba4331", + SwiftEmulatorHTTPPort: 8080, + } +} + +// NewMinio returns minio server, used as a local replacement for S3. +func (b Builder) NewMinio(bktName string) *e2e.HTTPService { + minioKESGithubContent := "https://raw.githubusercontent.com/minio/kes/master" + commands := []string{ + "curl -sSL --tlsv1.2 -O '%s/root.key' -O '%s/root.cert'", + "mkdir -p /data/%s && minio server --address :%v --quiet /data", + } + + m := e2e.NewHTTPService( + fmt.Sprintf("minio-%v", b.MinioHTTPPort), + b.MinioImage, + // Create the required bucket before starting minio. + e2e.NewCommandWithoutEntrypoint("sh", "-c", fmt.Sprintf(strings.Join(commands, " && "), minioKESGithubContent, minioKESGithubContent, bktName, b.MinioHTTPPort)), + e2e.NewHTTPReadinessProbe(b.MinioHTTPPort, "/minio/health/ready", 200, 200), + b.MinioHTTPPort, + ) + m.SetEnvVars(map[string]string{ + "MINIO_ACCESS_KEY": MinioAccessKey, + "MINIO_SECRET_KEY": MinioSecretKey, + "MINIO_BROWSER": "off", + "ENABLE_HTTPS": "0", + // https://docs.min.io/docs/minio-kms-quickstart-guide.html + "MINIO_KMS_KES_ENDPOINT": "https://play.min.io:7373", + "MINIO_KMS_KES_KEY_FILE": "root.key", + "MINIO_KMS_KES_CERT_FILE": "root.cert", + "MINIO_KMS_KES_KEY_NAME": "my-minio-key", + }) + return m +} + +func (b Builder) NewConsul() *e2e.HTTPService { + return e2e.NewHTTPService( + "consul", + b.ConsulImage, + // Run consul in "dev" mode so that the initial leader election is immediate. + e2e.NewCommand("agent", "-server", "-client=0.0.0.0", "-dev", "-log-level=err"), + e2e.NewHTTPReadinessProbe(b.ConsulHTTPPort, "/v1/operator/autopilot/health", 200, 200, `"Healthy": true`), + b.ConsulHTTPPort, + ) +} + +func (b Builder) NewETCD() *e2e.HTTPService { + return e2e.NewHTTPService( + "etcd", + b.EtcdImage, + e2e.NewCommand("/usr/local/bin/etcd", "--listen-client-urls=http://0.0.0.0:2379", "--advertise-client-urls=http://0.0.0.0:2379", "--listen-metrics-urls=http://0.0.0.0:9000", "--log-level=error"), + e2e.NewHTTPReadinessProbe(9000, "/health", 200, 204), + b.EtcdHTTPPort, + 9000, // Metrics. + ) +} + +func (b Builder) NewDynamoDB() *e2e.HTTPService { + return e2e.NewHTTPService( + "dynamodb", + b.DynamoDBImage, + e2e.NewCommand("-jar", "DynamoDBLocal.jar", "-inMemory", "-sharedDb"), + // DynamoDB doesn't have a readiness probe, so we check if the / works even if returns 400 + e2e.NewHTTPReadinessProbe(b.DynamoDBHTTPPort, "/", 400, 400), + b.DynamoDBHTTPPort, + ) +} + +func (b Builder) NewBigtable() *e2e.HTTPService { + return e2e.NewHTTPService( + "bigtable", + b.BigtableEmulatorImage, + nil, + nil, + b.BigtableEmulatorHTTPPort, + ) +} + +func (b Builder) NewCassandra() *e2e.HTTPService { + return e2e.NewHTTPService( + "cassandra", + b.CassandraImage, + nil, + // Readiness probe inspired from https://github.com/kubernetes/examples/blob/b86c9d50be45eaf5ce74dee7159ce38b0e149d38/cassandra/image/files/ready-probe.sh + e2e.NewCmdReadinessProbe(e2e.NewCommand("bash", "-c", "nodetool status | grep UN")), + b.CassandraHTTPPort, + ) +} + +func (b Builder) NewSwiftStorage() *e2e.HTTPService { + return e2e.NewHTTPService( + "swift", + b.SwiftEmulatorImage, + nil, + e2e.NewHTTPReadinessProbe(b.SwiftEmulatorHTTPPort, "/", 404, 404), + b.SwiftEmulatorHTTPPort, + ) +} + +func (b Builder) NewMemcached() *e2e.ConcreteService { + return e2e.NewConcreteService( + "memcached", + b.MemcachedImage, + nil, + e2e.NewTCPReadinessProbe(b.MemcachedHTTPPort), + b.MemcachedHTTPPort, + ) +} diff --git a/e2e/doc.go b/e2e/doc.go new file mode 100644 index 0000000..e526f2b --- /dev/null +++ b/e2e/doc.go @@ -0,0 +1,11 @@ +// Copyright (c) The EfficientGo Authors. +// Licensed under the Apache License 2.0. + +package e2e + +// This module is a fully featured e2e suite allowing utilizing `go test` for setting hermetic up complex microservice integration testing scenarios using docker. +// Example usages: +// * https://github.com/cortexproject/cortex/tree/master/integration +// * https://github.com/thanos-io/thanos/tree/master/test/e2e +// +// Check github.com/efficientgo/tools/e2e/db for common DBs services you can run out of the box. diff --git a/e2e/go.mod b/e2e/go.mod new file mode 100644 index 0000000..09aad1c --- /dev/null +++ b/e2e/go.mod @@ -0,0 +1,14 @@ +module github.com/efficientgo/tools/e2e + +go 1.15 + +require ( + github.com/efficientgo/tools/core v0.0.0-20210129204550-29f654ba5ec3 + github.com/go-kit/kit v0.10.0 + github.com/minio/minio-go/v7 v7.0.7 + github.com/pkg/errors v0.9.1 + github.com/prometheus/client_model v0.2.0 + github.com/prometheus/common v0.15.0 + gopkg.in/yaml.v2 v2.3.0 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c +) diff --git a/e2e/go.sum b/e2e/go.sum new file mode 100644 index 0000000..c93bdd2 --- /dev/null +++ b/e2e/go.sum @@ -0,0 +1,459 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +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= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +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 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +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-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +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/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +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/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +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/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +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= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +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/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +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.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +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/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/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= +github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= +github.com/minio/minio-go/v7 v7.0.7 h1:Qld/xb8C1Pwbu0jU46xAceyn9xXKCMW+3XfNbpmTB70= +github.com/minio/minio-go/v7 v7.0.7/go.mod h1:pEZBUa+L2m9oECoIA6IcSK8bv/qggtQVLovjeKK5jYc= +github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= +github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sio v0.2.1/go.mod h1:8b0yPp2avGThviy/+OCJBI6OMpvxoUuiLvE6F1lebhw= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +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/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +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= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +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/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +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/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/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.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +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.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/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +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.2.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.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM= +github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +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/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +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/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +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.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +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/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= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/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-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +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/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114 h1:DnSr2mCsxyCE6ZgIkmcWUQY2R5cH/6wL7eIxEmQOMSE= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +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= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/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= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +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.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +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/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= +gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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/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= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/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/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/e2e/internal/matchers/matcher.go b/e2e/internal/matchers/matcher.go new file mode 100644 index 0000000..815a953 --- /dev/null +++ b/e2e/internal/matchers/matcher.go @@ -0,0 +1,117 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package matchers + +import "fmt" + +// MatchType is an enum for label matching types. +type MatchType int + +// Possible MatchTypes. +const ( + MatchEqual MatchType = iota + MatchNotEqual + MatchRegexp + MatchNotRegexp +) + +func (m MatchType) String() string { + typeToStr := map[MatchType]string{ + MatchEqual: "=", + MatchNotEqual: "!=", + MatchRegexp: "=~", + MatchNotRegexp: "!~", + } + if str, ok := typeToStr[m]; ok { + return str + } + panic("unknown match type") +} + +// Matcher models the matching of a label. +type Matcher struct { + Type MatchType + Name string + Value string + + re *FastRegexMatcher +} + +// NewMatcher returns a matcher object. +func NewMatcher(t MatchType, n, v string) (*Matcher, error) { + m := &Matcher{ + Type: t, + Name: n, + Value: v, + } + if t == MatchRegexp || t == MatchNotRegexp { + re, err := NewFastRegexMatcher(v) + if err != nil { + return nil, err + } + m.re = re + } + return m, nil +} + +// MustNewMatcher panics on error - only for use in tests! +func MustNewMatcher(mt MatchType, name, val string) *Matcher { + m, err := NewMatcher(mt, name, val) + if err != nil { + panic(err) + } + return m +} + +func (m *Matcher) String() string { + return fmt.Sprintf("%s%s%q", m.Name, m.Type, m.Value) +} + +// Matches returns whether the matcher matches the given string value. +func (m *Matcher) Matches(s string) bool { + switch m.Type { + case MatchEqual: + return s == m.Value + case MatchNotEqual: + return s != m.Value + case MatchRegexp: + return m.re.MatchString(s) + case MatchNotRegexp: + return !m.re.MatchString(s) + } + panic("labels.Matcher.Matches: invalid match type") +} + +// Inverse returns a matcher that matches the opposite. +func (m *Matcher) Inverse() (*Matcher, error) { + switch m.Type { + case MatchEqual: + return NewMatcher(MatchNotEqual, m.Name, m.Value) + case MatchNotEqual: + return NewMatcher(MatchEqual, m.Name, m.Value) + case MatchRegexp: + return NewMatcher(MatchNotRegexp, m.Name, m.Value) + case MatchNotRegexp: + return NewMatcher(MatchRegexp, m.Name, m.Value) + } + panic("labels.Matcher.Matches: invalid match type") +} + +// GetRegexString returns the regex string. +func (m *Matcher) GetRegexString() string { + if m.re == nil { + return "" + } + return m.re.GetRegexString() +} diff --git a/e2e/internal/matchers/regexp.go b/e2e/internal/matchers/regexp.go new file mode 100644 index 0000000..9529ddc --- /dev/null +++ b/e2e/internal/matchers/regexp.go @@ -0,0 +1,107 @@ +// Copyright 2020 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package matchers + +import ( + "regexp" + "regexp/syntax" + "strings" +) + +type FastRegexMatcher struct { + re *regexp.Regexp + prefix string + suffix string + contains string +} + +func NewFastRegexMatcher(v string) (*FastRegexMatcher, error) { + re, err := regexp.Compile("^(?:" + v + ")$") + if err != nil { + return nil, err + } + + parsed, err := syntax.Parse(v, syntax.Perl) + if err != nil { + return nil, err + } + + m := &FastRegexMatcher{ + re: re, + } + + if parsed.Op == syntax.OpConcat { + m.prefix, m.suffix, m.contains = optimizeConcatRegex(parsed) + } + + return m, nil +} + +func (m *FastRegexMatcher) MatchString(s string) bool { + if m.prefix != "" && !strings.HasPrefix(s, m.prefix) { + return false + } + if m.suffix != "" && !strings.HasSuffix(s, m.suffix) { + return false + } + if m.contains != "" && !strings.Contains(s, m.contains) { + return false + } + return m.re.MatchString(s) +} + +func (m *FastRegexMatcher) GetRegexString() string { + return m.re.String() +} + +// optimizeConcatRegex returns literal prefix/suffix text that can be safely +// checked against the label value before running the regexp matcher. +func optimizeConcatRegex(r *syntax.Regexp) (prefix, suffix, contains string) { + sub := r.Sub + + // We can safely remove begin and end text matchers respectively + // at the beginning and end of the regexp. + if len(sub) > 0 && sub[0].Op == syntax.OpBeginText { + sub = sub[1:] + } + if len(sub) > 0 && sub[len(sub)-1].Op == syntax.OpEndText { + sub = sub[:len(sub)-1] + } + + if len(sub) == 0 { + return + } + + // Given Prometheus regex matchers are always anchored to the begin/end + // of the text, if the first/last operations are literals, we can safely + // treat them as prefix/suffix. + if sub[0].Op == syntax.OpLiteral && (sub[0].Flags&syntax.FoldCase) == 0 { + prefix = string(sub[0].Rune) + } + if last := len(sub) - 1; sub[last].Op == syntax.OpLiteral && (sub[last].Flags&syntax.FoldCase) == 0 { + suffix = string(sub[last].Rune) + } + + // If contains any literal which is not a prefix/suffix, we keep the + // 1st one. We do not keep the whole list of literals to simplify the + // fast path. + for i := 1; i < len(sub)-1; i++ { + if sub[i].Op == syntax.OpLiteral && (sub[i].Flags&syntax.FoldCase) == 0 { + contains = string(sub[i].Rune) + break + } + } + + return +} diff --git a/e2e/internal/s3/s3.go b/e2e/internal/s3/s3.go new file mode 100644 index 0000000..b1f04b6 --- /dev/null +++ b/e2e/internal/s3/s3.go @@ -0,0 +1,482 @@ +// Copyright (c) The Thanos Authors. +// Licensed under the Apache License 2.0. + +// Package s3 implements common object storage abstractions against s3-compatible APIs. +package s3 + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "os" + "runtime" + "strings" + "time" + + "github.com/efficientgo/tools/core/pkg/logerrcapture" + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/minio/minio-go/v7/pkg/encrypt" + "github.com/pkg/errors" + "github.com/prometheus/common/model" + "github.com/prometheus/common/version" + "gopkg.in/yaml.v2" +) + +const ( + // DirDelim is the delimiter used to model a directory structure in an object store bucket. + DirDelim = "/" + + // SSEKMS is the name of the SSE-KMS method for objectstore encryption. + SSEKMS = "SSE-KMS" + + // SSEC is the name of the SSE-C method for objstore encryption. + SSEC = "SSE-C" + + // SSES3 is the name of the SSE-S3 method for objstore encryption. + SSES3 = "SSE-S3" +) + +var DefaultConfig = Config{ + PutUserMetadata: map[string]string{}, + HTTPConfig: HTTPConfig{ + IdleConnTimeout: model.Duration(90 * time.Second), + ResponseHeaderTimeout: model.Duration(2 * time.Minute), + TLSHandshakeTimeout: model.Duration(10 * time.Second), + ExpectContinueTimeout: model.Duration(1 * time.Second), + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + MaxConnsPerHost: 0, + }, + // Minimum file size after which an HTTP multipart request should be used to upload objects to storage. + // Set to 128 MiB as in the minio client. + PartSize: 1024 * 1024 * 128, +} + +// Config stores the configuration for s3 bucket. +type Config struct { + Bucket string `yaml:"bucket"` + Endpoint string `yaml:"endpoint"` + Region string `yaml:"region"` + AccessKey string `yaml:"access_key"` + Insecure bool `yaml:"insecure"` + SignatureV2 bool `yaml:"signature_version2"` + SecretKey string `yaml:"secret_key"` + PutUserMetadata map[string]string `yaml:"put_user_metadata"` + HTTPConfig HTTPConfig `yaml:"http_config"` + TraceConfig TraceConfig `yaml:"trace"` + ListObjectsVersion string `yaml:"list_objects_version"` + // PartSize used for multipart upload. Only used if uploaded object size is known and larger than configured PartSize. + PartSize uint64 `yaml:"part_size"` + SSEConfig SSEConfig `yaml:"sse_config"` +} + +// SSEConfig deals with the configuration of SSE for Minio. The following options are valid: +// kmsencryptioncontext == https://docs.aws.amazon.com/kms/latest/developerguide/services-s3.html#s3-encryption-context +type SSEConfig struct { + Type string `yaml:"type"` + KMSKeyID string `yaml:"kms_key_id"` + KMSEncryptionContext map[string]string `yaml:"kms_encryption_context"` + EncryptionKey string `yaml:"encryption_key"` +} + +type TraceConfig struct { + Enable bool `yaml:"enable"` +} + +// HTTPConfig stores the http.Transport configuration for the s3 minio client. +type HTTPConfig struct { + IdleConnTimeout model.Duration `yaml:"idle_conn_timeout"` + ResponseHeaderTimeout model.Duration `yaml:"response_header_timeout"` + InsecureSkipVerify bool `yaml:"insecure_skip_verify"` + + TLSHandshakeTimeout model.Duration `yaml:"tls_handshake_timeout"` + ExpectContinueTimeout model.Duration `yaml:"expect_continue_timeout"` + MaxIdleConns int `yaml:"max_idle_conns"` + MaxIdleConnsPerHost int `yaml:"max_idle_conns_per_host"` + MaxConnsPerHost int `yaml:"max_conns_per_host"` + + // Allow upstream callers to inject a round tripper + Transport http.RoundTripper `yaml:"-"` +} + +// DefaultTransport - this default transport is based on the Minio +// DefaultTransport up until the following commit: +// https://github.com/minio/minio-go/commit/008c7aa71fc17e11bf980c209a4f8c4d687fc884 +// The values have since diverged. +func DefaultTransport(config Config) *http.Transport { + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + + MaxIdleConns: config.HTTPConfig.MaxIdleConns, + MaxIdleConnsPerHost: config.HTTPConfig.MaxIdleConnsPerHost, + IdleConnTimeout: time.Duration(config.HTTPConfig.IdleConnTimeout), + MaxConnsPerHost: config.HTTPConfig.MaxConnsPerHost, + TLSHandshakeTimeout: time.Duration(config.HTTPConfig.TLSHandshakeTimeout), + ExpectContinueTimeout: time.Duration(config.HTTPConfig.ExpectContinueTimeout), + // A custom ResponseHeaderTimeout was introduced + // to cover cases where the tcp connection works but + // the server never answers. Defaults to 2 minutes. + ResponseHeaderTimeout: time.Duration(config.HTTPConfig.ResponseHeaderTimeout), + // Set this value so that the underlying transport round-tripper + // doesn't try to auto decode the body of objects with + // content-encoding set to `gzip`. + // + // Refer: https://golang.org/src/net/http/transport.go?h=roundTrip#L1843. + DisableCompression: true, + TLSClientConfig: &tls.Config{InsecureSkipVerify: config.HTTPConfig.InsecureSkipVerify}, + } +} + +// Bucket implements the store.Bucket interface against s3-compatible APIs. +type Bucket struct { + logger log.Logger + name string + client *minio.Client + sse encrypt.ServerSide + putUserMetadata map[string]string + partSize uint64 + listObjectsV1 bool +} + +// parseConfig unmarshals a buffer into a Config with default HTTPConfig values. +func parseConfig(conf []byte) (Config, error) { + config := DefaultConfig + if err := yaml.UnmarshalStrict(conf, &config); err != nil { + return Config{}, err + } + + return config, nil +} + +// NewBucket returns a new Bucket using the provided s3 config values. +func NewBucket(logger log.Logger, conf []byte, component string) (*Bucket, error) { + config, err := parseConfig(conf) + if err != nil { + return nil, err + } + + return NewBucketWithConfig(logger, config, component) +} + +type overrideSignerType struct { + credentials.Provider + signerType credentials.SignatureType +} + +func (s *overrideSignerType) Retrieve() (credentials.Value, error) { + v, err := s.Provider.Retrieve() + if err != nil { + return v, err + } + if !v.SignerType.IsAnonymous() { + v.SignerType = s.signerType + } + return v, nil +} + +// NewBucketWithConfig returns a new Bucket using the provided s3 config values. +func NewBucketWithConfig(logger log.Logger, config Config, component string) (*Bucket, error) { + var chain []credentials.Provider + + // TODO(bwplotka): Don't do flags as they won't scale, use actual params like v2, v4 instead + wrapCredentialsProvider := func(p credentials.Provider) credentials.Provider { return p } + if config.SignatureV2 { + wrapCredentialsProvider = func(p credentials.Provider) credentials.Provider { + return &overrideSignerType{Provider: p, signerType: credentials.SignatureV2} + } + } + + if err := validate(config); err != nil { + return nil, err + } + if config.AccessKey != "" { + chain = []credentials.Provider{wrapCredentialsProvider(&credentials.Static{ + Value: credentials.Value{ + AccessKeyID: config.AccessKey, + SecretAccessKey: config.SecretKey, + SignerType: credentials.SignatureV4, + }, + })} + } else { + chain = []credentials.Provider{ + wrapCredentialsProvider(&credentials.EnvAWS{}), + wrapCredentialsProvider(&credentials.FileAWSCredentials{}), + wrapCredentialsProvider(&credentials.IAM{ + Client: &http.Client{ + Transport: http.DefaultTransport, + }, + }), + } + } + + // Check if a roundtripper has been set in the config + // otherwise build the default transport. + var rt http.RoundTripper + if config.HTTPConfig.Transport != nil { + rt = config.HTTPConfig.Transport + } else { + rt = DefaultTransport(config) + } + + client, err := minio.New(config.Endpoint, &minio.Options{ + Creds: credentials.NewChainCredentials(chain), + Secure: !config.Insecure, + Region: config.Region, + Transport: rt, + }) + if err != nil { + return nil, errors.Wrap(err, "initialize s3 client") + } + client.SetAppInfo(fmt.Sprintf("thanos-%s", component), fmt.Sprintf("%s (%s)", version.Version, runtime.Version())) + + var sse encrypt.ServerSide + if config.SSEConfig.Type != "" { + switch config.SSEConfig.Type { + case SSEKMS: + sse, err = encrypt.NewSSEKMS(config.SSEConfig.KMSKeyID, config.SSEConfig.KMSEncryptionContext) + if err != nil { + return nil, errors.Wrap(err, "initialize s3 client SSE-KMS") + } + + case SSEC: + key, err := ioutil.ReadFile(config.SSEConfig.EncryptionKey) + if err != nil { + return nil, err + } + + sse, err = encrypt.NewSSEC(key) + if err != nil { + return nil, errors.Wrap(err, "initialize s3 client SSE-C") + } + + case SSES3: + sse = encrypt.NewSSE() + + default: + sseErrMsg := errors.Errorf("Unsupported type %q was provided. Supported types are SSE-S3, SSE-KMS, SSE-C", config.SSEConfig.Type) + return nil, errors.Wrap(sseErrMsg, "Initialize s3 client SSE Config") + } + } + + if config.TraceConfig.Enable { + logWriter := log.NewStdlibAdapter(level.Debug(logger), log.MessageKey("s3TraceMsg")) + client.TraceOn(logWriter) + } + + if config.ListObjectsVersion != "" && config.ListObjectsVersion != "v1" && config.ListObjectsVersion != "v2" { + return nil, errors.Errorf("Initialize s3 client list objects version: Unsupported version %q was provided. Supported values are v1, v2", config.ListObjectsVersion) + } + + bkt := &Bucket{ + logger: logger, + name: config.Bucket, + client: client, + sse: sse, + putUserMetadata: config.PutUserMetadata, + partSize: config.PartSize, + listObjectsV1: config.ListObjectsVersion == "v1", + } + return bkt, nil +} + +// Name returns the bucket name for s3. +func (b *Bucket) Name() string { + return b.name +} + +// validate checks to see the config options are set. +func validate(conf Config) error { + if conf.Endpoint == "" { + return errors.New("no s3 endpoint in config file") + } + + if conf.AccessKey == "" && conf.SecretKey != "" { + return errors.New("no s3 acccess_key specified while secret_key is present in config file; either both should be present in config or envvars/IAM should be used.") + } + + if conf.AccessKey != "" && conf.SecretKey == "" { + return errors.New("no s3 secret_key specified while access_key is present in config file; either both should be present in config or envvars/IAM should be used.") + } + + if conf.SSEConfig.Type == SSEC && conf.SSEConfig.EncryptionKey == "" { + return errors.New("encryption_key must be set if sse_config.type is set to 'SSE-C'") + } + + if conf.SSEConfig.Type == SSEKMS && conf.SSEConfig.KMSKeyID == "" { + return errors.New("kms_key_id must be set if sse_config.type is set to 'SSE-KMS'") + } + + return nil +} + +// ValidateForTests checks to see the config options for tests are set. +func ValidateForTests(conf Config) error { + if conf.Endpoint == "" || + conf.AccessKey == "" || + conf.SecretKey == "" { + return errors.New("insufficient s3 test configuration information") + } + return nil +} + +// Iter calls f for each entry in the given directory. The argument to f is the full +// object name including the prefix of the inspected directory. +func (b *Bucket) Iter(ctx context.Context, dir string, f func(string) error) error { + // Ensure the object name actually ends with a dir suffix. Otherwise we'll just iterate the + // object itself as one prefix item. + if dir != "" { + dir = strings.TrimSuffix(dir, DirDelim) + DirDelim + } + + opts := minio.ListObjectsOptions{ + Prefix: dir, + Recursive: false, + UseV1: b.listObjectsV1, + } + + for object := range b.client.ListObjects(ctx, b.name, opts) { + // Catch the error when failed to list objects. + if object.Err != nil { + return object.Err + } + // This sometimes happens with empty buckets. + if object.Key == "" { + continue + } + // The s3 client can also return the directory itself in the ListObjects call above. + if object.Key == dir { + continue + } + if err := f(object.Key); err != nil { + return err + } + } + + return nil +} + +func (b *Bucket) getRange(ctx context.Context, name string, off, length int64) (io.ReadCloser, error) { + opts := &minio.GetObjectOptions{ServerSideEncryption: b.sse} + if length != -1 { + if err := opts.SetRange(off, off+length-1); err != nil { + return nil, err + } + } else if off > 0 { + if err := opts.SetRange(off, 0); err != nil { + return nil, err + } + } + r, err := b.client.GetObject(ctx, b.name, name, *opts) + if err != nil { + return nil, err + } + + // NotFoundObject error is revealed only after first Read. This does the initial GetRequest. Prefetch this here + // for convenience. + if _, err := r.Read(nil); err != nil { + logerrcapture.Do(b.logger, r.Close, "s3 get range obj close") + + // First GET Object request error. + return nil, err + } + + return r, nil +} + +// Get returns a reader for the given object name. +func (b *Bucket) Get(ctx context.Context, name string) (io.ReadCloser, error) { + return b.getRange(ctx, name, 0, -1) +} + +// GetRange returns a new range reader for the given object name and range. +func (b *Bucket) GetRange(ctx context.Context, name string, off, length int64) (io.ReadCloser, error) { + return b.getRange(ctx, name, off, length) +} + +// Exists checks if the given object exists. +func (b *Bucket) Exists(ctx context.Context, name string) (bool, error) { + _, err := b.client.StatObject(ctx, b.name, name, minio.StatObjectOptions{}) + if err != nil { + if b.IsObjNotFoundErr(err) { + return false, nil + } + return false, errors.Wrap(err, "stat s3 object") + } + + return true, nil +} + +// TryToGetSize tries to get upfront size from reader. +// TODO(https://github.com/thanos-io/thanos/issues/678): Remove guessing length when minio provider will support multipart upload without this. +func tryToGetSize(r io.Reader) (int64, error) { + switch f := r.(type) { + case *os.File: + fileInfo, err := f.Stat() + if err != nil { + return 0, errors.Wrap(err, "os.File.Stat()") + } + return fileInfo.Size(), nil + case *bytes.Buffer: + return int64(f.Len()), nil + case *strings.Reader: + return f.Size(), nil + } + return 0, errors.Errorf("unsupported type of io.Reader: %T", r) +} + +// Upload the contents of the reader as an object into the bucket. +func (b *Bucket) Upload(ctx context.Context, name string, r io.Reader) error { + // TODO(https://github.com/thanos-io/thanos/issues/678): Remove guessing length when minio provider will support multipart upload without this. + size, err := tryToGetSize(r) + if err != nil { + level.Warn(b.logger).Log("msg", "could not guess file size for multipart upload; upload might be not optimized", "name", name, "err", err) + size = -1 + } + + // partSize cannot be larger than object size. + partSize := b.partSize + if size < int64(partSize) { + partSize = 0 + } + if _, err := b.client.PutObject( + ctx, + b.name, + name, + r, + size, + minio.PutObjectOptions{ + PartSize: partSize, + ServerSideEncryption: b.sse, + UserMetadata: b.putUserMetadata, + }, + ); err != nil { + return errors.Wrap(err, "upload s3 object") + } + + return nil +} + +// Delete removes the object with the given name. +func (b *Bucket) Delete(ctx context.Context, name string) error { + return b.client.RemoveObject(ctx, b.name, name, minio.RemoveObjectOptions{}) +} + +// IsObjNotFoundErr returns true if error means that object is not found. Relevant to Get operations. +func (b *Bucket) IsObjNotFoundErr(err error) bool { + return minio.ToErrorResponse(err).Code == "NoSuchKey" +} + +func (b *Bucket) Close() error { return nil } diff --git a/e2e/metrics.go b/e2e/metrics.go new file mode 100644 index 0000000..45ec6ae --- /dev/null +++ b/e2e/metrics.go @@ -0,0 +1,212 @@ +package e2e + +import ( + "math" + + "github.com/efficientgo/tools/e2e/internal/matchers" + io_prometheus_client "github.com/prometheus/client_model/go" +) + +// GetMetricValueFunc defined the signature of a function used to get the metric value. +type getMetricValueFunc func(m *io_prometheus_client.Metric) float64 + +// MetricsOption defined the signature of a function used to manipulate options. +type MetricsOption func(*metricsOptions) + +// metricsOptions is the structure holding all options. +type metricsOptions struct { + getValue getMetricValueFunc + labelMatchers []*matchers.Matcher + waitMissingMetrics bool + skipMissingMetrics bool +} + +// WithMetricCount is an option to get the histogram/summary count as metric value. +func WithMetricCount() MetricsOption { + return func(o *metricsOptions) { + o.getValue = getMetricCount + } +} + +// WithLabelMatchers is an option to filter only matching series. +func WithLabelMatchers(matchers ...*matchers.Matcher) MetricsOption { + return func(o *metricsOptions) { + o.labelMatchers = matchers + } +} + +// WithWaitMissingMetrics is an option to wait whenever an expected metric is missing. If this +// option is not enabled, will return error on missing metrics. +func WaitMissingMetrics() MetricsOption { + return func(o *metricsOptions) { + o.waitMissingMetrics = true + } +} + +// SkipWaitMissingMetrics is an option to skip/ignore whenever an expected metric is missing. +func SkipMissingMetrics() MetricsOption { + return func(o *metricsOptions) { + o.skipMissingMetrics = true + } +} + +func buildMetricsOptions(opts []MetricsOption) metricsOptions { + result := metricsOptions{ + getValue: getMetricValue, + } + for _, opt := range opts { + opt(&result) + } + return result +} + +func getMetricValue(m *io_prometheus_client.Metric) float64 { + if m.GetGauge() != nil { + return m.GetGauge().GetValue() + } else if m.GetCounter() != nil { + return m.GetCounter().GetValue() + } else if m.GetHistogram() != nil { + return m.GetHistogram().GetSampleSum() + } else if m.GetSummary() != nil { + return m.GetSummary().GetSampleSum() + } else { + return 0 + } +} + +func getMetricCount(m *io_prometheus_client.Metric) float64 { + if m.GetHistogram() != nil { + return float64(m.GetHistogram().GetSampleCount()) + } else if m.GetSummary() != nil { + return float64(m.GetSummary().GetSampleCount()) + } else { + return 0 + } +} + +func getValues(metrics []*io_prometheus_client.Metric, opts metricsOptions) []float64 { + values := make([]float64, 0, len(metrics)) + for _, m := range metrics { + values = append(values, opts.getValue(m)) + } + return values +} + +func filterMetrics(metrics []*io_prometheus_client.Metric, opts metricsOptions) []*io_prometheus_client.Metric { + // If no label matcher is configured, then no filtering should be done. + if len(opts.labelMatchers) == 0 { + return metrics + } + if len(metrics) == 0 { + return metrics + } + + filtered := make([]*io_prometheus_client.Metric, 0, len(metrics)) + + for _, m := range metrics { + metricLabels := map[string]string{} + for _, lp := range m.GetLabel() { + metricLabels[lp.GetName()] = lp.GetValue() + } + + matches := true + for _, matcher := range opts.labelMatchers { + if !matcher.Matches(metricLabels[matcher.Name]) { + matches = false + break + } + } + + if !matches { + continue + } + + filtered = append(filtered, m) + } + + return filtered +} + +func SumValues(values []float64) float64 { + sum := 0.0 + for _, v := range values { + sum += v + } + return sum +} + +func EqualsSingle(expected float64) func(float64) bool { + return func(v float64) bool { + return v == expected || (math.IsNaN(v) && math.IsNaN(expected)) + } +} + +// Equals is an isExpected function for WaitSumMetrics that returns true if given single sum is equals to given value. +func Equals(value float64) func(sums ...float64) bool { + return func(sums ...float64) bool { + if len(sums) != 1 { + panic("equals: expected one value") + } + return sums[0] == value || math.IsNaN(sums[0]) && math.IsNaN(value) + } +} + +// Greater is an isExpected function for WaitSumMetrics that returns true if given single sum is greater than given value. +func Greater(value float64) func(sums ...float64) bool { + return func(sums ...float64) bool { + if len(sums) != 1 { + panic("greater: expected one value") + } + return sums[0] > value + } +} + +// GreaterOrEqual is an isExpected function for WaitSumMetrics that returns true if given single sum is greater or equal than given value. +func GreaterOrEqual(value float64) func(sums ...float64) bool { + return func(sums ...float64) bool { + if len(sums) != 1 { + panic("greater: expected one value") + } + return sums[0] >= value + } +} + +// Less is an isExpected function for WaitSumMetrics that returns true if given single sum is less than given value. +func Less(value float64) func(sums ...float64) bool { + return func(sums ...float64) bool { + if len(sums) != 1 { + panic("less: expected one value") + } + return sums[0] < value + } +} + +// EqualsAmongTwo is an isExpected function for WaitSumMetrics that returns true if first sum is equal to the second. +// NOTE: Be careful on scrapes in between of process that changes two metrics. Those are +// usually not atomic. +func EqualsAmongTwo(sums ...float64) bool { + if len(sums) != 2 { + panic("equalsAmongTwo: expected two values") + } + return sums[0] == sums[1] +} + +// GreaterAmongTwo is an isExpected function for WaitSumMetrics that returns true if first sum is greater than second. +// NOTE: Be careful on scrapes in between of process that changes two metrics. Those are +// usually not atomic. +func GreaterAmongTwo(sums ...float64) bool { + if len(sums) != 2 { + panic("greaterAmongTwo: expected two values") + } + return sums[0] > sums[1] +} + +// LessAmongTwo is an isExpected function for WaitSumMetrics that returns true if first sum is smaller than second. +// NOTE: Be careful on scrapes in between of process that changes two metrics. Those are +// usually not atomic. +func LessAmongTwo(sums ...float64) bool { + if len(sums) != 2 { + panic("lessAmongTwo: expected two values") + } + return sums[0] < sums[1] +} diff --git a/e2e/scenario.go b/e2e/scenario.go new file mode 100644 index 0000000..08e7a8d --- /dev/null +++ b/e2e/scenario.go @@ -0,0 +1,297 @@ +package e2e + +import ( + "crypto/rand" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "github.com/go-kit/kit/log" + "github.com/pkg/errors" +) + +const ContainerSharedDir = "/shared" + +// Service is unified service interface that Scenario can manage. +type Service interface { + Name() string + Start(logger log.Logger, networkName, dir string) error + WaitReady() error + + // It should be ok to Stop and Kill more than once, with next invokes being noop. + Kill() error + Stop() error +} + +type logger struct { + w io.Writer +} + +func NewLogger(w io.Writer) *logger { + return &logger{ + w: w, + } +} + +func (l *logger) Log(keyvals ...interface{}) error { + b := strings.Builder{} + b.WriteString(time.Now().Format("15:04:05")) + + for _, v := range keyvals { + b.WriteString(" " + fmt.Sprint(v)) + } + + b.WriteString("\n") + + _, err := l.w.Write([]byte(b.String())) + return err +} + +// Scenario allows to manage deployments for single testing scenario. +type Scenario struct { + o scenarioOptions + sharedDir string + + services []Service +} + +// ScenarioOption defined the signature of a function used to manipulate options. +type ScenarioOption func(*scenarioOptions) + +type scenarioOptions struct { + networkName string + logger log.Logger +} + +// WithNetworkName tells scenario to use custom network name instead of UUID. +func WithNetworkName(networkName string) ScenarioOption { + return func(o *scenarioOptions) { + o.networkName = networkName + } +} + +// WithLogger tells scenario to use custom logger default one (stdout). +func WithLogger(logger log.Logger) ScenarioOption { + return func(o *scenarioOptions) { + o.logger = logger + } +} + +// NewScenario creates new Scenario. +func NewScenario(opts ...ScenarioOption) (_ *Scenario, err error) { + s := &Scenario{} + for _, o := range opts { + o(&s.o) + } + if s.o.networkName == "" { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + return nil, err + } + s.o.networkName = fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) + } + if s.o.logger == nil { + s.o.logger = NewLogger(os.Stdout) + } + + s.sharedDir, err = getTempDirectory() + if err != nil { + return nil, err + } + + // Force a shutdown in order to cleanup from a spurious situation in case + // the previous tests run didn't cleanup correctly. + s.shutdown() + + // Setup the docker network. + if out, err := RunCommandAndGetOutput("docker", "network", "create", s.o.networkName); err != nil { + s.o.logger.Log(string(out)) + s.clean() + return nil, errors.Wrapf(err, "create docker network '%s'", s.o.networkName) + } + + return s, nil +} + +// SharedDir returns the absolute path of the directory on the host that is shared with all services in docker. +func (s *Scenario) SharedDir() string { + return s.sharedDir +} + +// NetworkName returns the network name that scenario is responsible for. +func (s *Scenario) NetworkName() string { + return s.o.networkName +} + +func (s *Scenario) isRegistered(name string) bool { + for _, service := range s.services { + if service.Name() == name { + return true + } + } + return false +} + +func (s *Scenario) StartAndWaitReady(services ...Service) error { + if err := s.Start(services...); err != nil { + return err + } + return s.WaitReady(services...) +} + +func (s *Scenario) Start(services ...Service) error { + for _, service := range services { + s.o.logger.Log("Starting", service.Name()) + + // Ensure another service with the same name doesn't exist. + if s.isRegistered(service.Name()) { + return fmt.Errorf("another service with the same name '%s' has already been started", service.Name()) + } + + // Start the service. + if err := service.Start(s.o.logger, s.o.networkName, s.SharedDir()); err != nil { + return err + } + + // Add to the list of services. + s.services = append(s.services, service) + } + + return nil +} + +func (s *Scenario) Stop(services ...Service) error { + for _, service := range services { + if !s.isRegistered(service.Name()) { + return fmt.Errorf("unable to stop service %s because it does not exist", service.Name()) + } + if err := service.Stop(); err != nil { + return err + } + + // Remove the service from the list of services. + for i, entry := range s.services { + if entry.Name() == service.Name() { + s.services = append(s.services[:i], s.services[i+1:]...) + break + } + } + } + return nil +} + +func (s *Scenario) WaitReady(services ...Service) error { + for _, service := range services { + if !s.isRegistered(service.Name()) { + return fmt.Errorf("unable to wait for service %s because it does not exist", service.Name()) + } + if err := service.WaitReady(); err != nil { + return err + } + } + return nil +} + +func (s *Scenario) Close() { + if s == nil { + return + } + s.shutdown() + s.clean() +} + +// TODO(bwplotka): Add comments. +func (s *Scenario) clean() { + if err := os.RemoveAll(s.sharedDir); err != nil { + s.o.logger.Log("error while removing sharedDir", s.sharedDir, "err:", err) + } +} + +func (s *Scenario) shutdown() { + // Kill the services in the opposite order. + for i := len(s.services) - 1; i >= 0; i-- { + if err := s.services[i].Kill(); err != nil { + s.o.logger.Log("Unable to kill service", s.services[i].Name(), ":", err.Error()) + } + } + + // Ensure there are no leftover containers. + if out, err := RunCommandAndGetOutput( + "docker", + "ps", + "-a", + "--quiet", + "--filter", + fmt.Sprintf("network=%s", s.o.networkName), + ); err == nil { + for _, containerID := range strings.Split(string(out), "\n") { + containerID = strings.TrimSpace(containerID) + if containerID == "" { + continue + } + + if out, err = RunCommandAndGetOutput("docker", "rm", "--force", containerID); err != nil { + s.o.logger.Log(string(out)) + s.o.logger.Log("Unable to cleanup leftover container", containerID, ":", err.Error()) + } + } + } else { + s.o.logger.Log(string(out)) + s.o.logger.Log("Unable to cleanup leftover containers:", err.Error()) + } + + // Teardown the docker network. In case the network does not exists (ie. this function + // is called during the setup of the scenario) we skip the removal in order to not log + // an error which may be misleading. + if ok, err := existDockerNetwork(s.o.logger, s.o.networkName); ok || err != nil { + if out, err := RunCommandAndGetOutput("docker", "network", "rm", s.o.networkName); err != nil { + s.o.logger.Log(string(out)) + s.o.logger.Log("Unable to remove docker network", s.o.networkName, ":", err.Error()) + } + } +} + +func existDockerNetwork(logger log.Logger, networkName string) (bool, error) { + out, err := RunCommandAndGetOutput("docker", "network", "ls", "--quiet", "--filter", fmt.Sprintf("name=%s", networkName)) + if err != nil { + logger.Log(string(out)) + logger.Log("Unable to check if docker network", networkName, "exists:", err.Error()) + } + + return strings.TrimSpace(string(out)) != "", nil +} + +// getTempDirectory creates a temporary directory for shared integration +// test files, either in the working directory or a directory referenced by +// the E2E_TEMP_DIR environment variable. +func getTempDirectory() (string, error) { + var ( + dir string + err error + ) + // If a temp dir is referenced, return that + if os.Getenv("E2E_TEMP_DIR") != "" { + dir = os.Getenv("E2E_TEMP_DIR") + } else { + dir, err = os.Getwd() + if err != nil { + return "", err + } + } + + tmpDir, err := ioutil.TempDir(dir, "e2e_integration_test") + if err != nil { + return "", err + } + absDir, err := filepath.Abs(tmpDir) + if err != nil { + _ = os.RemoveAll(tmpDir) + return "", err + } + + return absDir, nil +} diff --git a/e2e/scenario_test.go b/e2e/scenario_test.go new file mode 100644 index 0000000..75b8b1d --- /dev/null +++ b/e2e/scenario_test.go @@ -0,0 +1,147 @@ +package e2e_test + +import ( + "bytes" + "context" + "io/ioutil" + "testing" + "time" + + "github.com/efficientgo/tools/core/pkg/testutil" + "github.com/efficientgo/tools/e2e" + e2edb "github.com/efficientgo/tools/e2e/db" + "github.com/efficientgo/tools/e2e/internal/s3" + "github.com/go-kit/kit/log" + "gopkg.in/yaml.v3" +) + +const bktName = "cheesecake" + +func spinup(t *testing.T, networkName string) (*e2e.Scenario, *e2e.HTTPService, *e2e.HTTPService) { + s, err := e2e.NewScenario(e2e.WithNetworkName(networkName)) + testutil.Ok(t, err) + + m1 := e2edb.Default().NewMinio(bktName) + + d := e2edb.Default() + d.MinioHTTPPort = 9001 + m2 := d.NewMinio(bktName) + + closePlease := true + defer func() { + if closePlease { + // You're welcome. + s.Close() + } + }() + testutil.Ok(t, s.StartAndWaitReady(m1, m2)) + testutil.NotOk(t, s.Start(m1)) + testutil.NotOk(t, s.Start(e2edb.Default().NewMinio(bktName))) + + closePlease = false + return s, m1, m2 +} + +// TODO(bwplotka): Get rid of minio example and test scenario with just raw server and some HTTP, no need to minio client deps. +func testMinioWorking(t *testing.T, m *e2e.HTTPService) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + b, err := yaml.Marshal(s3.Config{ + Endpoint: m.HTTPEndpoint(), + Bucket: bktName, + AccessKey: e2edb.MinioAccessKey, + SecretKey: e2edb.MinioSecretKey, + Insecure: true, // WARNING: Our secret cheesecake recipes might leak. + }) + testutil.Ok(t, err) + + bkt, err := s3.NewBucket(log.NewNopLogger(), b, "test") + testutil.Ok(t, err) + + testutil.Ok(t, bkt.Upload(ctx, "recipe", bytes.NewReader([]byte("Just go to Pastry Shop and buy.")))) + testutil.Ok(t, bkt.Upload(ctx, "mom/recipe", bytes.NewReader([]byte("https://www.bbcgoodfood.com/recipes/strawberry-cheesecake-4-easy-steps")))) + + r, err := bkt.Get(ctx, "recipe") + testutil.Ok(t, err) + b, err = ioutil.ReadAll(r) + testutil.Ok(t, err) + testutil.Equals(t, "Just go to Pastry Shop and buy.", string(b)) + + r, err = bkt.Get(ctx, "mom/recipe") + testutil.Ok(t, err) + b, err = ioutil.ReadAll(r) + testutil.Ok(t, err) + testutil.Equals(t, "https://www.bbcgoodfood.com/recipes/strawberry-cheesecake-4-easy-steps", string(b)) +} + +func TestScenario(t *testing.T) { + t.Parallel() + + s, m1, m2 := spinup(t, "e2e-scenario-test") + defer s.Close() + + t.Run("minio is working", func(t *testing.T) { + testMinioWorking(t, m1) + testMinioWorking(t, m2) + }) + + t.Run("concurrent nested scenario 1 is working just fine as well", func(t *testing.T) { + t.Parallel() + + s, m1, m2 := spinup(t, "e2e-scenario-test1") + defer s.Close() + + testMinioWorking(t, m1) + testMinioWorking(t, m2) + }) + t.Run("concurrent nested scenario 2 is working just fine as well", func(t *testing.T) { + t.Parallel() + + s, m1, m2 := spinup(t, "e2e-scenario-test2") + defer s.Close() + + testMinioWorking(t, m1) + testMinioWorking(t, m2) + }) + + testutil.Ok(t, s.Stop(m1)) + + // Expect m1 not working. + b, err := yaml.Marshal(s3.Config{ + Endpoint: m1.Name(), + Bucket: "cheescake", + AccessKey: e2edb.MinioAccessKey, + SecretKey: e2edb.MinioSecretKey, + }) + testutil.Ok(t, err) + bkt, err := s3.NewBucket(log.NewNopLogger(), b, "test") + testutil.Ok(t, err) + + _, err = bkt.Get(context.Background(), "recipe") + testutil.NotOk(t, err) + + testMinioWorking(t, m2) + + testutil.NotOk(t, s.Stop(m1)) + // Should be noop. + testutil.Ok(t, m1.Stop()) + // I can run closes as many times I want. + s.Close() + s.Close() + s.Close() + + // Expect m2 not working. + b, err = yaml.Marshal(s3.Config{ + Endpoint: m2.Name(), + Bucket: "cheescake", + AccessKey: e2edb.MinioAccessKey, + SecretKey: e2edb.MinioSecretKey, + }) + testutil.Ok(t, err) + bkt, err = s3.NewBucket(log.NewNopLogger(), b, "test") + testutil.Ok(t, err) + + _, err = bkt.Get(context.Background(), "recipe") + testutil.NotOk(t, err) +} diff --git a/e2e/service.go b/e2e/service.go new file mode 100644 index 0000000..e28c14c --- /dev/null +++ b/e2e/service.go @@ -0,0 +1,674 @@ +package e2e + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "net" + "net/http" + "os/exec" + "regexp" + "strconv" + "strings" + "time" + + "github.com/efficientgo/tools/core/pkg/backoff" + "github.com/efficientgo/tools/core/pkg/errcapture" + "github.com/go-kit/kit/log" + "github.com/pkg/errors" + "github.com/prometheus/common/expfmt" +) + +var ( + dockerPortPattern = regexp.MustCompile(`^.*:(\d+)$`) + errMissingMetric = errors.New("metric not found") +) + +// ConcreteService represents microservice with optional ports which will be discoverable from docker +// with :. For connecting from test, use `Endpoint` method. +// +// ConcreteService can be reused (started and stopped many time), but it can represent only one running container +// at the time. +type ConcreteService struct { + name string + image string + networkPorts []int + env map[string]string + user string + command *Command + readiness ReadinessProbe + + // Maps container ports to dynamically binded local ports. + networkPortsContainerToLocal map[int]int + + // Generic backoff backoff. + backoff *backoff.Backoff + + // docker NetworkName used to start this container. + // If empty it means service is stopped. + usedNetworkName string + + // Available after start only. + logger log.Logger +} + +func NewConcreteService( + name string, + image string, + command *Command, + readiness ReadinessProbe, + networkPorts ...int, +) *ConcreteService { + return &ConcreteService{ + name: name, + image: image, + networkPorts: networkPorts, + command: command, + networkPortsContainerToLocal: map[int]int{}, + readiness: readiness, + backoff: backoff.New(context.Background(), backoff.Config{ + Min: 300 * time.Millisecond, + Max: 600 * time.Millisecond, + MaxRetries: 50, // Sometimes the CI is slow ¯\_(ツ)_/¯ + }), + } +} + +func (s *ConcreteService) isExpectedRunning() bool { + return s.usedNetworkName != "" +} + +func (s *ConcreteService) Name() string { return s.name } + +// Less often used options. + +func (s *ConcreteService) SetBackoff(cfg backoff.Config) { + s.backoff = backoff.New(context.Background(), cfg) +} + +func (s *ConcreteService) SetEnvVars(env map[string]string) { + s.env = env +} + +func (s *ConcreteService) SetUser(user string) { + s.user = user +} + +func (s *ConcreteService) Start(logger log.Logger, networkName, sharedDir string) (err error) { + s.logger = logger + // In case of any error, if the container was already created, we + // have to cleanup removing it. We ignore the error of the "docker rm" + // because we don't know if the container was created or not. + defer func() { + if err != nil { + _, _ = RunCommandAndGetOutput("docker", "rm", "--force", s.name) + } + }() + + cmd := exec.Command("docker", s.buildDockerRunArgs(networkName, sharedDir)...) + cmd.Stdout = &LinePrefixLogger{prefix: s.name + ": ", logger: logger} + cmd.Stderr = &LinePrefixLogger{prefix: s.name + ": ", logger: logger} + if err = cmd.Start(); err != nil { + return err + } + s.usedNetworkName = networkName + + // Wait until the container has been started. + if err = s.WaitForRunning(); err != nil { + return err + } + + // Get the dynamic local ports mapped to the container. + for _, containerPort := range s.networkPorts { + var out []byte + var localPort int + + out, err = RunCommandAndGetOutput("docker", "port", s.containerName(), strconv.Itoa(containerPort)) + if err != nil { + // Catch init errors. + if werr := s.WaitForRunning(); werr != nil { + return errors.Wrapf(werr, "failed to get mapping for port as container %s exited: %v", s.containerName(), err) + } + return errors.Wrapf(err, "unable to get mapping for port %d; service: %s; output: %q", containerPort, s.name, out) + } + + stdout := strings.TrimSpace(string(out)) + matches := dockerPortPattern.FindStringSubmatch(stdout) + if len(matches) != 2 { + return fmt.Errorf("unable to get mapping for port %d (output: %s); service: %s", containerPort, stdout, s.name) + } + + localPort, err = strconv.Atoi(matches[1]) + if err != nil { + return errors.Wrapf(err, "unable to get mapping for port %d; service: %s", containerPort, s.name) + } + s.networkPortsContainerToLocal[containerPort] = localPort + } + s.logger.Log("Ports for container:", s.containerName(), "Mapping:", s.networkPortsContainerToLocal) + return nil +} + +func (s *ConcreteService) Stop() error { + if !s.isExpectedRunning() { + return nil + } + + s.logger.Log("Stopping", s.name) + + if out, err := RunCommandAndGetOutput("docker", "stop", "--time=30", s.containerName()); err != nil { + s.logger.Log(string(out)) + return err + } + s.usedNetworkName = "" + + return nil +} + +func (s *ConcreteService) Kill() error { + if !s.isExpectedRunning() { + return nil + } + + s.logger.Log("Killing", s.name) + + if out, err := RunCommandAndGetOutput("docker", "kill", s.containerName()); err != nil { + s.logger.Log(string(out)) + return err + } + + // Wait until the container actually stopped. However, this could fail if + // the container already exited, so we just ignore the error. + _, _ = RunCommandAndGetOutput("docker", "wait", s.containerName()) + + s.usedNetworkName = "" + + return nil +} + +// Endpoint returns external (from host perspective) service endpoint (host:port) for given internal port. +// External means that it will be accessible only from host, but not from docker containers. +// +// If your service is not running, this method returns incorrect `stopped` endpoint. +func (s *ConcreteService) Endpoint(port int) string { + if !s.isExpectedRunning() { + return "stopped" + } + + // Map the container port to the local port. + localPort, ok := s.networkPortsContainerToLocal[port] + if !ok { + return "" + } + + // Do not use "localhost" cause it doesn't work with the AWS DynamoDB client. + return fmt.Sprintf("127.0.0.1:%d", localPort) +} + +// NetworkEndpoint returns internal service endpoint (host:port) for given internal port. +// Internal means that it will be accessible only from docker containers within the network that this +// service is running in. If you configure your local resolver with docker DNS namespace you can access it from host +// as well. Use `Endpoint` for host access. +// +// If your service is not running, use `NetworkEndpointFor` instead. +func (s *ConcreteService) NetworkEndpoint(port int) string { + if s.usedNetworkName == "" { + return "stopped" + } + return s.NetworkEndpointFor(s.usedNetworkName, port) +} + +// NetworkEndpointFor returns internal service endpoint (host:port) for given internal port and network. +// Internal means that it will be accessible only from docker containers within the given network. If you configure +// your local resolver with docker DNS namespace you can access it from host as well. +// +// This method return correct endpoint for the service in any state. +func (s *ConcreteService) NetworkEndpointFor(networkName string, port int) string { + return fmt.Sprintf("%s:%d", NetworkContainerHost(networkName, s.name), port) +} + +func (s *ConcreteService) SetReadinessProbe(probe ReadinessProbe) { + s.readiness = probe +} + +func (s *ConcreteService) Ready() error { + if !s.isExpectedRunning() { + return fmt.Errorf("service %s is stopped", s.Name()) + } + + // Ensure the service has a readiness probe configure. + if s.readiness == nil { + return nil + } + + return s.readiness.Ready(s) +} + +func (s *ConcreteService) containerName() string { + return NetworkContainerHost(s.usedNetworkName, s.name) +} + +func (s *ConcreteService) WaitForRunning() (err error) { + if !s.isExpectedRunning() { + return fmt.Errorf("service %s is stopped", s.Name()) + } + + for s.backoff.Reset(); s.backoff.Ongoing(); { + // Enforce a timeout on the command execution because we've seen some flaky tests + // stuck here. + + var out []byte + out, err = RunCommandWithTimeoutAndGetOutput(5*time.Second, "docker", "inspect", "--format={{json .State.Running}}", s.containerName()) + if err != nil { + s.backoff.Wait() + continue + } + + if out == nil { + err = fmt.Errorf("nil output") + s.backoff.Wait() + continue + } + + str := strings.TrimSpace(string(out)) + if str != "true" { + err = fmt.Errorf("unexpected output: %q", str) + s.backoff.Wait() + continue + } + + return nil + } + + return fmt.Errorf("docker container %s failed to start: %v", s.name, err) +} + +func (s *ConcreteService) WaitReady() (err error) { + if !s.isExpectedRunning() { + return fmt.Errorf("service %s is stopped", s.Name()) + } + + for s.backoff.Reset(); s.backoff.Ongoing(); { + err = s.Ready() + if err == nil { + return nil + } + + s.backoff.Wait() + } + + return fmt.Errorf("the service %s is not ready; err: %v", s.name, err) +} + +func (s *ConcreteService) buildDockerRunArgs(networkName, sharedDir string) []string { + args := []string{"run", "--rm", "--net=" + networkName, "--name=" + networkName + "-" + s.name, "--hostname=" + s.name} + + // Mount the shared/ directory into the container + args = append(args, "-v", fmt.Sprintf("%s:%s:z", sharedDir, ContainerSharedDir)) + + // Environment variables + for name, value := range s.env { + args = append(args, "-e", name+"="+value) + } + + if s.user != "" { + args = append(args, "--user", s.user) + } + + // Published ports + for _, port := range s.networkPorts { + args = append(args, "-p", strconv.Itoa(port)) + } + + // Disable entrypoint if required + if s.command != nil && s.command.entrypointDisabled { + args = append(args, "--entrypoint", "") + } + + args = append(args, s.image) + + if s.command != nil { + args = append(args, s.command.cmd) + args = append(args, s.command.args...) + } + + return args +} + +// Exec runs the provided against a the docker container specified by this +// service. It returns the stdout, stderr, and error response from attempting +// to run the command. +func (s *ConcreteService) Exec(command *Command) (string, string, error) { + args := []string{"exec", s.containerName()} + args = append(args, command.cmd) + args = append(args, command.args...) + + cmd := exec.Command("docker", args...) + var stdout bytes.Buffer + cmd.Stdout = &stdout + + var stderr bytes.Buffer + cmd.Stderr = &stderr + + err := cmd.Run() + + return stdout.String(), stderr.String(), err +} + +// NetworkContainerHost return the hostname of the container within the network. This is +// the address a container should use to connect to other containers. +func NetworkContainerHost(networkName, containerName string) string { + return fmt.Sprintf("%s-%s", networkName, containerName) +} + +// NetworkContainerHostPort return the host:port address of a container within the network. +func NetworkContainerHostPort(networkName, containerName string, port int) string { + return fmt.Sprintf("%s-%s:%d", networkName, containerName, port) +} + +type Command struct { + cmd string + args []string + entrypointDisabled bool +} + +func NewCommand(cmd string, args ...string) *Command { + return &Command{ + cmd: cmd, + args: args, + } +} + +func NewCommandWithoutEntrypoint(cmd string, args ...string) *Command { + return &Command{ + cmd: cmd, + args: args, + entrypointDisabled: true, + } +} + +type ReadinessProbe interface { + Ready(service *ConcreteService) (err error) +} + +// HTTPReadinessProbe checks readiness by making HTTP call and checking for expected HTTP status code +type HTTPReadinessProbe struct { + port int + path string + expectedStatusRangeStart int + expectedStatusRangeEnd int + expectedContent []string +} + +func NewHTTPReadinessProbe(port int, path string, expectedStatusRangeStart, expectedStatusRangeEnd int, expectedContent ...string) *HTTPReadinessProbe { + return &HTTPReadinessProbe{ + port: port, + path: path, + expectedStatusRangeStart: expectedStatusRangeStart, + expectedStatusRangeEnd: expectedStatusRangeEnd, + expectedContent: expectedContent, + } +} + +func (p *HTTPReadinessProbe) Ready(service *ConcreteService) (err error) { + endpoint := service.Endpoint(p.port) + if endpoint == "" { + return fmt.Errorf("cannot get service endpoint for port %d", p.port) + } else if endpoint == "stopped" { + return errors.New("service has stopped") + } + + res, err := (&http.Client{Timeout: 1 * time.Second}).Get("http://" + endpoint + p.path) + if err != nil { + return err + } + + defer errcapture.ExhaustClose(&err, res.Body, "response readiness") + body, _ := ioutil.ReadAll(res.Body) + + if res.StatusCode < p.expectedStatusRangeStart || res.StatusCode > p.expectedStatusRangeEnd { + return fmt.Errorf("expected code in range: [%v, %v], got status code: %v and body: %v", p.expectedStatusRangeStart, p.expectedStatusRangeEnd, res.StatusCode, string(body)) + } + + for _, expected := range p.expectedContent { + if !strings.Contains(string(body), expected) { + return fmt.Errorf("expected body containing %s, got: %v", expected, string(body)) + } + } + + return nil +} + +// TCPReadinessProbe checks readiness by ensure a TCP connection can be established. +type TCPReadinessProbe struct { + port int +} + +func NewTCPReadinessProbe(port int) *TCPReadinessProbe { + return &TCPReadinessProbe{ + port: port, + } +} + +func (p *TCPReadinessProbe) Ready(service *ConcreteService) (err error) { + endpoint := service.Endpoint(p.port) + if endpoint == "" { + return fmt.Errorf("cannot get service endpoint for port %d", p.port) + } else if endpoint == "stopped" { + return errors.New("service has stopped") + } + + conn, err := net.DialTimeout("tcp", endpoint, time.Second) + if err != nil { + return err + } + + return conn.Close() +} + +// CmdReadinessProbe checks readiness by `Exec`ing a command (within container) which returns 0 to consider status being ready +type CmdReadinessProbe struct { + cmd *Command +} + +func NewCmdReadinessProbe(cmd *Command) *CmdReadinessProbe { + return &CmdReadinessProbe{cmd: cmd} +} + +func (p *CmdReadinessProbe) Ready(service *ConcreteService) error { + _, _, err := service.Exec(p.cmd) + return err +} + +type LinePrefixLogger struct { + prefix string + logger log.Logger +} + +func (w *LinePrefixLogger) Write(p []byte) (n int, err error) { + for _, line := range strings.Split(string(p), "\n") { + // Skip empty lines + line = strings.TrimSpace(line) + if line == "" { + continue + } + + // Write the prefix + line to the wrapped writer + if err := w.logger.Log(w.prefix + line); err != nil { + return 0, err + } + } + + return len(p), nil +} + +// HTTPService represents opinionated microservice with at least HTTP port that as mandatory requirement, +// serves metrics. +type HTTPService struct { + *ConcreteService + + httpPort int +} + +func NewHTTPService( + name string, + image string, + command *Command, + readiness ReadinessProbe, + httpPort int, + otherPorts ...int, +) *HTTPService { + return &HTTPService{ + ConcreteService: NewConcreteService(name, image, command, readiness, append(otherPorts, httpPort)...), + httpPort: httpPort, + } +} + +func (s *HTTPService) Metrics() (_ string, err error) { + // Map the container port to the local port + localPort := s.networkPortsContainerToLocal[s.httpPort] + + // Fetch metrics. + res, err := (&http.Client{Timeout: 5 * time.Second}).Get(fmt.Sprintf("http://localhost:%d/metrics", localPort)) + if err != nil { + return "", err + } + + // Check the status code. + if res.StatusCode < 200 || res.StatusCode >= 300 { + return "", fmt.Errorf("unexpected status code %d while fetching metrics", res.StatusCode) + } + + defer errcapture.ExhaustClose(&err, res.Body, "metrics response") + body, err := ioutil.ReadAll(res.Body) + + return string(body), err +} + +func (s *HTTPService) HTTPPort() int { + return s.httpPort +} + +func (s *HTTPService) HTTPEndpoint() string { + return s.Endpoint(s.httpPort) +} + +func (s *HTTPService) NetworkHTTPEndpoint() string { + return s.NetworkEndpoint(s.httpPort) +} + +func (s *HTTPService) NetworkHTTPEndpointFor(networkName string) string { + return s.NetworkEndpointFor(networkName, s.httpPort) +} + +// WaitSumMetrics waits for at least one instance of each given metric names to be present and their sums, returning true +// when passed to given isExpected(...). +func (s *HTTPService) WaitSumMetrics(isExpected func(sums ...float64) bool, metricNames ...string) error { + return s.WaitSumMetricsWithOptions(isExpected, metricNames) +} + +func (s *HTTPService) WaitSumMetricsWithOptions(isExpected func(sums ...float64) bool, metricNames []string, opts ...MetricsOption) error { + var ( + sums []float64 + err error + options = buildMetricsOptions(opts) + ) + + for s.backoff.Reset(); s.backoff.Ongoing(); { + sums, err = s.SumMetrics(metricNames, opts...) + if options.waitMissingMetrics && errors.Is(err, errMissingMetric) { + continue + } + if err != nil { + return err + } + + if isExpected(sums...) { + return nil + } + + s.backoff.Wait() + } + + return fmt.Errorf("unable to find metrics %s with expected values. Last error: %v. Last values: %v", metricNames, err, sums) +} + +// SumMetrics returns the sum of the values of each given metric names. +func (s *HTTPService) SumMetrics(metricNames []string, opts ...MetricsOption) ([]float64, error) { + options := buildMetricsOptions(opts) + sums := make([]float64, len(metricNames)) + + metrics, err := s.Metrics() + if err != nil { + return nil, err + } + + var tp expfmt.TextParser + families, err := tp.TextToMetricFamilies(strings.NewReader(metrics)) + if err != nil { + return nil, err + } + + for i, m := range metricNames { + sums[i] = 0.0 + + // Get the metric family. + mf, ok := families[m] + if !ok { + if options.skipMissingMetrics { + continue + } + + return nil, errors.Wrapf(errMissingMetric, "metric=%s service=%s", m, s.name) + } + + // Filter metrics. + metrics := filterMetrics(mf.GetMetric(), options) + if len(metrics) == 0 { + if options.skipMissingMetrics { + continue + } + + return nil, errors.Wrapf(errMissingMetric, "metric=%s service=%s", m, s.name) + } + + sums[i] = SumValues(getValues(metrics, options)) + } + + return sums, nil +} + +// WaitRemovedMetric waits until a metric disappear from the list of metrics exported by the service. +func (s *HTTPService) WaitRemovedMetric(metricName string, opts ...MetricsOption) error { + options := buildMetricsOptions(opts) + + for s.backoff.Reset(); s.backoff.Ongoing(); { + // Fetch metrics. + metrics, err := s.Metrics() + if err != nil { + return err + } + + // Parse metrics. + var tp expfmt.TextParser + families, err := tp.TextToMetricFamilies(strings.NewReader(metrics)) + if err != nil { + return err + } + + // Get the metric family. + mf, ok := families[metricName] + if !ok { + return nil + } + + // Filter metrics. + if len(filterMetrics(mf.GetMetric(), options)) == 0 { + return nil + } + + s.backoff.Wait() + } + + return fmt.Errorf("the metric %s is still exported by %s", metricName, s.name) +} diff --git a/e2e/service_test.go b/e2e/service_test.go new file mode 100644 index 0000000..2eecda5 --- /dev/null +++ b/e2e/service_test.go @@ -0,0 +1,169 @@ +package e2e + +import ( + "math" + "net" + "net/http" + "strconv" + "testing" + "time" + + "github.com/efficientgo/tools/core/pkg/backoff" + "github.com/efficientgo/tools/core/pkg/testutil" +) + +func TestWaitSumMetric(t *testing.T) { + // Listen on a random port before starting the HTTP server, to + // make sure the port is already open when we'll call WaitSumMetric() + // the first time (this avoid flaky tests). + ln, err := net.Listen("tcp", "localhost:0") + testutil.Ok(t, err) + defer ln.Close() + + // Get the port. + _, addrPort, err := net.SplitHostPort(ln.Addr().String()) + testutil.Ok(t, err) + + port, err := strconv.Atoi(addrPort) + testutil.Ok(t, err) + + // Start an HTTP server exposing the metrics. + srv := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(` +# HELP metric_c cheescake +# TYPE metric_c gauge +metric_c 20 +# HELP metric_a cheescake +# TYPE metric_a gauge +metric_a 1 +metric_a{first="value1"} 10 +metric_a{first="value1", something="x"} 4 +metric_a{first="value1", something2="a"} 203 +metric_a{first="value2"} 2 +metric_a{second="value1"} 1 +# HELP metric_b cheescake +# TYPE metric_b gauge +metric_b 1000 +# HELP metric_b_counter cheescake +# TYPE metric_b_counter counter +metric_b_counter 1020 +# HELP metric_b_hist cheescake +# TYPE metric_b_hist histogram +metric_b_hist_count 5 +metric_b_hist_sum 124 +metric_b_hist_bucket{le="5.36870912e+08"} 1 +metric_b_hist_bucket{le="+Inf"} 5 +# HELP metric_b_summary cheescake +# TYPE metric_b_summary summary +metric_b_summary_sum 22 +metric_b_summary_count 1 +`)) + }), + } + defer srv.Close() + + go func() { + _ = srv.Serve(ln) + }() + + s := &HTTPService{ + httpPort: 0, + ConcreteService: &ConcreteService{ + networkPortsContainerToLocal: map[int]int{ + 0: port, + }, + }, + } + + s.SetBackoff(backoff.Config{ + Min: 300 * time.Millisecond, + Max: 600 * time.Millisecond, + MaxRetries: 50, + }) + testutil.Ok(t, s.WaitSumMetrics(Equals(221), "metric_a")) + + // No retry. + s.SetBackoff(backoff.Config{ + Min: 0, + Max: 0, + MaxRetries: 1, + }) + testutil.NotOk(t, s.WaitSumMetrics(Equals(16), "metric_a")) + + testutil.Ok(t, s.WaitSumMetrics(Equals(1000), "metric_b")) + testutil.Ok(t, s.WaitSumMetrics(Equals(1020), "metric_b_counter")) + testutil.Ok(t, s.WaitSumMetrics(Equals(124), "metric_b_hist")) + testutil.Ok(t, s.WaitSumMetrics(Equals(22), "metric_b_summary")) + + testutil.Ok(t, s.WaitSumMetrics(EqualsAmongTwo, "metric_a", "metric_a")) + testutil.NotOk(t, s.WaitSumMetrics(EqualsAmongTwo, "metric_a", "metric_b")) + + testutil.Ok(t, s.WaitSumMetrics(GreaterAmongTwo, "metric_b", "metric_a")) + testutil.NotOk(t, s.WaitSumMetrics(GreaterAmongTwo, "metric_a", "metric_b")) + + testutil.Ok(t, s.WaitSumMetrics(LessAmongTwo, "metric_a", "metric_b")) + testutil.NotOk(t, s.WaitSumMetrics(LessAmongTwo, "metric_b", "metric_a")) + + testutil.NotOk(t, s.WaitSumMetrics(Equals(0), "non_existing_metric")) +} + +func TestWaitSumMetric_Nan(t *testing.T) { + // Listen on a random port before starting the HTTP server, to + // make sure the port is already open when we'll call WaitSumMetric() + // the first time (this avoid flaky tests). + ln, err := net.Listen("tcp", "localhost:0") + testutil.Ok(t, err) + defer ln.Close() + + // Get the port. + _, addrPort, err := net.SplitHostPort(ln.Addr().String()) + testutil.Ok(t, err) + + port, err := strconv.Atoi(addrPort) + testutil.Ok(t, err) + + // Start an HTTP server exposing the metrics. + srv := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(` +# HELP metric_c cheescake +# TYPE metric_c GAUGE +metric_c 20 +# HELP metric_a cheescake +# TYPE metric_a GAUGE +metric_a 1 +metric_a{first="value1"} 10 +metric_a{first="value1", something="x"} 4 +metric_a{first="value1", something2="a"} 203 +metric_a{first="value1", something3="b"} Nan +metric_a{first="value2"} 2 +metric_a{second="value1"} 1 +# HELP metric_b cheescake +# TYPE metric_b GAUGE +metric_b 1000 +`)) + }), + } + defer srv.Close() + + go func() { + _ = srv.Serve(ln) + }() + + s := &HTTPService{ + httpPort: 0, + ConcreteService: &ConcreteService{ + networkPortsContainerToLocal: map[int]int{ + 0: port, + }, + }, + } + + s.SetBackoff(backoff.Config{ + Min: 300 * time.Millisecond, + Max: 600 * time.Millisecond, + MaxRetries: 50, + }) + testutil.Ok(t, s.WaitSumMetrics(Equals(math.NaN()), "metric_a")) +} From 0c9f1cc6545c0fbaf69833216910387674626a10 Mon Sep 17 00:00:00 2001 From: Bartlomiej Plotka Date: Fri, 29 Jan 2021 20:52:52 +0000 Subject: [PATCH 2/2] Fix. Signed-off-by: Bartlomiej Plotka --- Makefile | 2 +- core/go.sum | 4 ---- core/pkg/backoff/backoff.go | 3 +++ core/pkg/backoff/backoff_test.go | 3 +++ e2e/cli.go | 3 +++ e2e/composite_service.go | 3 +++ e2e/db/db.go | 3 +++ e2e/go.mod | 2 +- e2e/go.sum | 5 +++-- e2e/internal/matchers/matcher.go | 3 +++ e2e/internal/matchers/regexp.go | 3 +++ e2e/internal/s3/s3.go | 3 +++ e2e/metrics.go | 3 +++ e2e/scenario.go | 8 ++++++-- e2e/scenario_test.go | 3 +++ e2e/service.go | 9 ++++++--- e2e/service_test.go | 3 +++ 17 files changed, 50 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index b81e2ef..45016af 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,7 @@ lint: $(FAILLINT) $(GOLANGCI_LINT) $(MISSPELL) build format docs check-git deps $(call require_clean_work_tree,"detected not clean master before running lint - run make lint and commit changes.") @echo ">> verifying imported " for dir in $(MODULES) ; do \ - cd $${dir} && $(FAILLINT) -paths "fmt.{Print,PrintfPrintln,Sprint}" -ignore-tests ./...; && \ + cd $${dir} && $(FAILLINT) -paths "fmt.{Print,PrintfPrintln,Sprint}" -ignore-tests ./... && \ $(FAILLINT) -paths "github.com/stretchr/testify=github.com/efficientgo/tools/core/pkg/testutil" ./...; \ done @echo ">> examining all of the Go files" diff --git a/core/go.sum b/core/go.sum index f7759a5..aa414b2 100644 --- a/core/go.sum +++ b/core/go.sum @@ -13,8 +13,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -34,5 +32,3 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/core/pkg/backoff/backoff.go b/core/pkg/backoff/backoff.go index f32105e..deab524 100644 --- a/core/pkg/backoff/backoff.go +++ b/core/pkg/backoff/backoff.go @@ -1,3 +1,6 @@ +// Copyright (c) The EfficientGo Authors. +// Licensed under the Apache License 2.0. + package backoff // Copied from https://github.com/cortexproject/cortex/blob/0ec7b9664a01d538f1f49580b4c359a5c3cc755a/pkg/util/backoff.go diff --git a/core/pkg/backoff/backoff_test.go b/core/pkg/backoff/backoff_test.go index c209672..aaf11bc 100644 --- a/core/pkg/backoff/backoff_test.go +++ b/core/pkg/backoff/backoff_test.go @@ -1,3 +1,6 @@ +// Copyright (c) The EfficientGo Authors. +// Licensed under the Apache License 2.0. + package backoff // Copied from https://github.com/cortexproject/cortex/blob/0ec7b9664a01d538f1f49580b4c359a5c3cc755a/pkg/util/backoff.go diff --git a/e2e/cli.go b/e2e/cli.go index b999461..f4bf1ca 100644 --- a/e2e/cli.go +++ b/e2e/cli.go @@ -1,3 +1,6 @@ +// Copyright (c) The EfficientGo Authors. +// Licensed under the Apache License 2.0. + package e2e import ( diff --git a/e2e/composite_service.go b/e2e/composite_service.go index 1165d82..7eb0500 100644 --- a/e2e/composite_service.go +++ b/e2e/composite_service.go @@ -1,3 +1,6 @@ +// Copyright (c) The EfficientGo Authors. +// Licensed under the Apache License 2.0. + package e2e import ( diff --git a/e2e/db/db.go b/e2e/db/db.go index b6ffeed..55cf262 100644 --- a/e2e/db/db.go +++ b/e2e/db/db.go @@ -1,3 +1,6 @@ +// Copyright (c) The EfficientGo Authors. +// Licensed under the Apache License 2.0. + package e2edb import ( diff --git a/e2e/go.mod b/e2e/go.mod index 09aad1c..99519aa 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -3,7 +3,7 @@ module github.com/efficientgo/tools/e2e go 1.15 require ( - github.com/efficientgo/tools/core v0.0.0-20210129204550-29f654ba5ec3 + github.com/efficientgo/tools/core v0.0.0-20210129205121-421d0828c9a6 github.com/go-kit/kit v0.10.0 github.com/minio/minio-go/v7 v7.0.7 github.com/pkg/errors v0.9.1 diff --git a/e2e/go.sum b/e2e/go.sum index c93bdd2..33049bf 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -50,6 +50,8 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/efficientgo/tools/core v0.0.0-20210129205121-421d0828c9a6 h1:UMDR56yUMYNVtwMCL6DccrxuYBVDTRRHPWIb8bMa1tk= +github.com/efficientgo/tools/core v0.0.0-20210129205121-421d0828c9a6/go.mod h1:OmVcnJopJL8d3X3sSXTiypGoUSgFq1aDGmlrdi9dn/M= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -288,9 +290,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= diff --git a/e2e/internal/matchers/matcher.go b/e2e/internal/matchers/matcher.go index 815a953..0f673d6 100644 --- a/e2e/internal/matchers/matcher.go +++ b/e2e/internal/matchers/matcher.go @@ -1,3 +1,6 @@ +// Copyright (c) The EfficientGo Authors. +// Licensed under the Apache License 2.0. + // Copyright 2017 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/e2e/internal/matchers/regexp.go b/e2e/internal/matchers/regexp.go index 9529ddc..a0d21b9 100644 --- a/e2e/internal/matchers/regexp.go +++ b/e2e/internal/matchers/regexp.go @@ -1,3 +1,6 @@ +// Copyright (c) The EfficientGo Authors. +// Licensed under the Apache License 2.0. + // Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/e2e/internal/s3/s3.go b/e2e/internal/s3/s3.go index b1f04b6..2e91539 100644 --- a/e2e/internal/s3/s3.go +++ b/e2e/internal/s3/s3.go @@ -1,3 +1,6 @@ +// Copyright (c) The EfficientGo Authors. +// Licensed under the Apache License 2.0. + // Copyright (c) The Thanos Authors. // Licensed under the Apache License 2.0. diff --git a/e2e/metrics.go b/e2e/metrics.go index 45ec6ae..b7fe4ab 100644 --- a/e2e/metrics.go +++ b/e2e/metrics.go @@ -1,3 +1,6 @@ +// Copyright (c) The EfficientGo Authors. +// Licensed under the Apache License 2.0. + package e2e import ( diff --git a/e2e/scenario.go b/e2e/scenario.go index 08e7a8d..bbcaf7a 100644 --- a/e2e/scenario.go +++ b/e2e/scenario.go @@ -1,3 +1,6 @@ +// Copyright (c) The EfficientGo Authors. +// Licensed under the Apache License 2.0. + package e2e import ( @@ -42,7 +45,7 @@ func (l *logger) Log(keyvals ...interface{}) error { b.WriteString(time.Now().Format("15:04:05")) for _, v := range keyvals { - b.WriteString(" " + fmt.Sprint(v)) + b.WriteString(" " + fmt.Sprintf("%v", v)) } b.WriteString("\n") @@ -260,6 +263,7 @@ func existDockerNetwork(logger log.Logger, networkName string) (bool, error) { if err != nil { logger.Log(string(out)) logger.Log("Unable to check if docker network", networkName, "exists:", err.Error()) + return false, err } return strings.TrimSpace(string(out)) != "", nil @@ -273,7 +277,7 @@ func getTempDirectory() (string, error) { dir string err error ) - // If a temp dir is referenced, return that + // If a temp dir is referenced, return that. if os.Getenv("E2E_TEMP_DIR") != "" { dir = os.Getenv("E2E_TEMP_DIR") } else { diff --git a/e2e/scenario_test.go b/e2e/scenario_test.go index 75b8b1d..fd0f99e 100644 --- a/e2e/scenario_test.go +++ b/e2e/scenario_test.go @@ -1,3 +1,6 @@ +// Copyright (c) The EfficientGo Authors. +// Licensed under the Apache License 2.0. + package e2e_test import ( diff --git a/e2e/service.go b/e2e/service.go index e28c14c..2d98787 100644 --- a/e2e/service.go +++ b/e2e/service.go @@ -1,3 +1,6 @@ +// Copyright (c) The EfficientGo Authors. +// Licensed under the Apache License 2.0. + package e2e import ( @@ -391,7 +394,7 @@ type ReadinessProbe interface { Ready(service *ConcreteService) (err error) } -// HTTPReadinessProbe checks readiness by making HTTP call and checking for expected HTTP status code +// HTTPReadinessProbe checks readiness by making HTTP call and checking for expected HTTP status code. type HTTPReadinessProbe struct { port int path string @@ -466,7 +469,7 @@ func (p *TCPReadinessProbe) Ready(service *ConcreteService) (err error) { return conn.Close() } -// CmdReadinessProbe checks readiness by `Exec`ing a command (within container) which returns 0 to consider status being ready +// CmdReadinessProbe checks readiness by `Exec`ing a command (within container) which returns 0 to consider status being ready. type CmdReadinessProbe struct { cmd *Command } @@ -525,7 +528,7 @@ func NewHTTPService( } func (s *HTTPService) Metrics() (_ string, err error) { - // Map the container port to the local port + // Map the container port to the local port. localPort := s.networkPortsContainerToLocal[s.httpPort] // Fetch metrics. diff --git a/e2e/service_test.go b/e2e/service_test.go index 2eecda5..2a2824f 100644 --- a/e2e/service_test.go +++ b/e2e/service_test.go @@ -1,3 +1,6 @@ +// Copyright (c) The EfficientGo Authors. +// Licensed under the Apache License 2.0. + package e2e import (