From 8a7b73f41f758a2de27b10bbd58f6085e4ca5a48 Mon Sep 17 00:00:00 2001 From: Umputun Date: Thu, 1 Apr 2021 02:37:28 -0500 Subject: [PATCH] initial version #1 --- .github/CODEOWNERS | 5 + .github/workflows/ci.yml | 53 +++++ .gitignore | 15 +- app/discovery/discovery.go | 116 +++++++++++ app/discovery/discovery_test.go | 102 +++++++++ app/discovery/provider/docker.go | 163 +++++++++++++++ app/discovery/provider/docker_client_mock.go | 114 ++++++++++ app/discovery/provider/docker_test.go | 64 ++++++ app/discovery/provider/file.go | 91 ++++++++ app/discovery/provider/file_test.go | 63 ++++++ app/discovery/provider/static.go | 41 ++++ app/discovery/provider/testdata/routes.txt | 4 + app/discovery/provider_mock.go | 146 +++++++++++++ app/main.go | 157 ++++++++++++++ app/proxy/middleware/gzip.go | 48 +++++ app/proxy/middleware/gzip_test.go | 62 ++++++ app/proxy/proxy.go | 133 ++++++++++++ app/proxy/proxy_test.go | 60 ++++++ go.mod | 12 ++ go.sum | 206 +++++++++++++++++++ 20 files changed, 1642 insertions(+), 13 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/ci.yml create mode 100644 app/discovery/discovery.go create mode 100644 app/discovery/discovery_test.go create mode 100644 app/discovery/provider/docker.go create mode 100644 app/discovery/provider/docker_client_mock.go create mode 100644 app/discovery/provider/docker_test.go create mode 100644 app/discovery/provider/file.go create mode 100644 app/discovery/provider/file_test.go create mode 100644 app/discovery/provider/static.go create mode 100644 app/discovery/provider/testdata/routes.txt create mode 100644 app/discovery/provider_mock.go create mode 100644 app/main.go create mode 100644 app/proxy/middleware/gzip.go create mode 100644 app/proxy/middleware/gzip_test.go create mode 100644 app/proxy/proxy.go create mode 100644 app/proxy/proxy_test.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..91c94cb --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# These owners will be the default owners for everything in the repo. +# Unless a later match takes precedence, @umputun will be requested for +# review when someone opens a pull request. + +* @umputun diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1cdaaf8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: build + +on: + push: + tags: [v*] + branches: + pull_request: + repository_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: set up go 1.16 + uses: actions/setup-go@v2 + with: + go-version: 1.16 + id: go + + - name: checkout + uses: actions/checkout@v2 + + - name: build and test + run: | + go test -v -timeout=100s -covermode=count -coverprofile=$GITHUB_WORKSPACE/profile.cov_tmp ./... + cat $GITHUB_WORKSPACE/profile.cov_tmp | grep -v "mocks" | grep -v "_mock" > $GITHUB_WORKSPACE/profile.cov + working-directory: backend/app + env: + GOFLAGS: "-mod=vendor" + TZ: "America/Chicago" + + - name: install golangci-lint and goveralls + run: | + curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GITHUB_WORKSPACE v1.29.0 + GO111MODULE=off go get -u github.com/mattn/goveralls + + - name: run linters + run: $GITHUB_WORKSPACE/golangci-lint run --out-format=github-actions + working-directory: backend/app + env: + GOFLAGS: "-mod=vendor" + TZ: "America/Chicago" + + - name: build and deploy image + env: + GITHUB_PACKAGE_TOKEN: ${{ secrets.PKG_TOKEN }} + run: | + ref="$(echo ${GITHUB_REF} | cut -d'/' -f3)" + echo GITHUB_REF - $ref + docker login docker.pkg.github.com -u umputun -p ${GITHUB_PACKAGE_TOKEN} + docker build -t docker.pkg.github.com/umputun/docker-proxy/dpx:${ref} . + docker push docker.pkg.github.com/umputun/docker-proxy/dpx:${ref} diff --git a/.gitignore b/.gitignore index 66fd13c..dbced62 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,4 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` *.test - -# Output of the go coverage tool, specifically when used with LiteIDE *.out - -# Dependency directories (remove the comment below to include it) -# vendor/ +.runconf +var/ \ No newline at end of file diff --git a/app/discovery/discovery.go b/app/discovery/discovery.go new file mode 100644 index 0000000..2e77a3a --- /dev/null +++ b/app/discovery/discovery.go @@ -0,0 +1,116 @@ +// Package discovery provides a common interface for all providers and Match to +// transform source to destination URL. +// Do func starts event loop checking all providers and retrieving lists of rules. +// All lists combined into a merged one. +package discovery + +import ( + "context" + "regexp" + "sync" +) + +//go:generate moq -out provider_mock.go -fmt goimports . Provider + +// Service implements discovery with multiple providers and url matcher +type Service struct { + providers []Provider + mappers []UrlMapper + lock sync.RWMutex +} + +// UrlMapper contains all info about source and destination routes +type UrlMapper struct { + SrcMatch *regexp.Regexp + Dst string + ProviderID string +} + +// Provider defines sources of mappers +type Provider interface { + Events(ctx context.Context) (res <-chan struct{}) + List() (res []UrlMapper, err error) + ID() string +} + +// NewService makes service with given providers +func NewService(providers []Provider) *Service { + return &Service{providers: providers} +} + +// Do runs blocking loop getting events from all providers +// and updating mappers on each event +func (s *Service) Do(ctx context.Context) error { + var evChs []<-chan struct{} + for _, p := range s.providers { + evChs = append(evChs, p.Events(ctx)) + } + ch := s.mergeEvents(ctx, evChs...) + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ch: + m := s.mergeLists() + s.lock.Lock() + s.mappers = make([]UrlMapper, len(m)) + copy(s.mappers, m) + s.lock.Unlock() + } + } +} + +// Match url to all providers mappers +func (s *Service) Match(url string) (string, bool) { + s.lock.RLock() + defer s.lock.RUnlock() + for _, m := range s.mappers { + dest := m.SrcMatch.ReplaceAllString(url, m.Dst) + if url != dest { + return dest, true + } + } + return url, false +} + +func (s *Service) mergeLists() (res []UrlMapper) { + for _, p := range s.providers { + lst, err := p.List() + if err != nil { + continue + } + res = append(res, lst...) + } + return res +} + +func (s *Service) mergeEvents(ctx context.Context, chs ...<-chan struct{}) <-chan struct{} { + var wg sync.WaitGroup + out := make(chan struct{}) + + output := func(ctx context.Context, c <-chan struct{}) { + defer wg.Done() + for { + select { + case <-ctx.Done(): + return + case v, ok := <-c: + if !ok { + return + } + out <- v + } + } + } + + wg.Add(len(chs)) + for _, c := range chs { + go output(ctx, c) + } + + go func() { + wg.Wait() + close(out) + }() + return out +} diff --git a/app/discovery/discovery_test.go b/app/discovery/discovery_test.go new file mode 100644 index 0000000..d9d5d15 --- /dev/null +++ b/app/discovery/discovery_test.go @@ -0,0 +1,102 @@ +package discovery + +import ( + "context" + "regexp" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestService_Do(t *testing.T) { + p1 := &ProviderMock{ + EventsFunc: func(ctx context.Context) <-chan struct{} { + res := make(chan struct{}, 1) + res <- struct{}{} + return res + }, + ListFunc: func() ([]UrlMapper, error) { + return []UrlMapper{ + {SrcMatch: regexp.MustCompile("^/api/svc1/(.*)"), Dst: "http://127.0.0.1:8080/blah1/$1"}, + {SrcMatch: regexp.MustCompile("^/api/svc2/(.*)"), Dst: "http://127.0.0.2:8080/blah2/$1/abc"}, + }, nil + }, + } + p2 := &ProviderMock{ + EventsFunc: func(ctx context.Context) <-chan struct{} { + return make(chan struct{}, 1) + }, + ListFunc: func() ([]UrlMapper, error) { + return []UrlMapper{ + {SrcMatch: regexp.MustCompile("/api/svc3/xyz"), Dst: "http://127.0.0.3:8080/blah3/xyz"}, + }, nil + }, + } + svc := NewService([]Provider{p1, p2}) + + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + err := svc.Do(ctx) + require.Error(t, err) + assert.Equal(t, context.DeadlineExceeded, err) + assert.Equal(t, 3, len(svc.mappers)) + + assert.Equal(t, 1, len(p1.EventsCalls())) + assert.Equal(t, 1, len(p2.EventsCalls())) + assert.Equal(t, 1, len(p1.ListCalls())) + assert.Equal(t, 1, len(p2.ListCalls())) +} + +func TestService_Match(t *testing.T) { + p1 := &ProviderMock{ + EventsFunc: func(ctx context.Context) <-chan struct{} { + res := make(chan struct{}, 1) + res <- struct{}{} + return res + }, + ListFunc: func() ([]UrlMapper, error) { + return []UrlMapper{ + {SrcMatch: regexp.MustCompile("^/api/svc1/(.*)"), Dst: "http://127.0.0.1:8080/blah1/$1"}, + {SrcMatch: regexp.MustCompile("^/api/svc2/(.*)"), Dst: "http://127.0.0.2:8080/blah2/$1/abc"}, + }, nil + }, + } + p2 := &ProviderMock{ + EventsFunc: func(ctx context.Context) <-chan struct{} { + return make(chan struct{}, 1) + }, + ListFunc: func() ([]UrlMapper, error) { + return []UrlMapper{ + {SrcMatch: regexp.MustCompile("/api/svc3/xyz"), Dst: "http://127.0.0.3:8080/blah3/xyz"}, + }, nil + }, + } + svc := NewService([]Provider{p1, p2}) + + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + err := svc.Do(ctx) + require.Error(t, err) + assert.Equal(t, context.DeadlineExceeded, err) + assert.Equal(t, 3, len(svc.mappers)) + + { + res, ok := svc.Match("/api/svc3/xyz") + assert.True(t, ok) + assert.Equal(t, "http://127.0.0.3:8080/blah3/xyz", res) + } + { + res, ok := svc.Match("/api/svc1/1234") + assert.True(t, ok) + assert.Equal(t, "http://127.0.0.1:8080/blah1/1234", res) + } + { + res, ok := svc.Match("/aaa/api/svc1/1234") + assert.False(t, ok) + assert.Equal(t, "/aaa/api/svc1/1234", res) + } +} diff --git a/app/discovery/provider/docker.go b/app/discovery/provider/docker.go new file mode 100644 index 0000000..db88a98 --- /dev/null +++ b/app/discovery/provider/docker.go @@ -0,0 +1,163 @@ +package provider + +import ( + "context" + "fmt" + "regexp" + "strings" + "time" + + dclient "github.com/fsouza/go-dockerclient" + log "github.com/go-pkgz/lgr" + "github.com/pkg/errors" + + "github.com/umputun/docker-proxy/app/discovery" +) + +//go:generate moq -out docker_client_mock.go -skip-ensure -fmt goimports . DockerClient + +// Docker emits all changes from all containers states +type Docker struct { + DockerClient DockerClient + Excludes []string +} + +// DockerClient defines interface listing containers and subscribing to events +type DockerClient interface { + ListContainers(opts dclient.ListContainersOptions) ([]dclient.APIContainers, error) + AddEventListener(listener chan<- *dclient.APIEvents) error +} + +// containerInfo is simplified docker.APIEvents for containers only +type containerInfo struct { + ID string + Name string + TS time.Time + Labels map[string]string +} + +var ( + upStatuses = []string{"start", "restart"} + downStatuses = []string{"die", "destroy", "stop", "pause"} +) + +// Channel gets eventsCh with all containers events +func (s *Docker) Events(ctx context.Context) (res <-chan struct{}) { + eventsCh := make(chan struct{}) + go func() { + for { + err := s.events(ctx, s.DockerClient, eventsCh) + if err == context.Canceled || err == context.DeadlineExceeded { + close(eventsCh) + return + } + log.Printf("[WARN] docker events listener failed, restarted, %v", err) + time.Sleep(100 * time.Millisecond) + } + }() + return eventsCh +} + +// List all containers and make url mappers +func (s *Docker) List() ([]discovery.UrlMapper, error) { + containers, err := s.listContainers() + if err != nil { + return nil, err + } + + var res []discovery.UrlMapper + for _, c := range containers { + srcURL := fmt.Sprintf("^/api/%s/(.*)", c.Name) + destURL := fmt.Sprintf("http://%s:8080/$1", c.Name) + if v, ok := c.Labels["dpx.route"]; ok { + srcURL = v + } + if v, ok := c.Labels["dpx.dest"]; ok { + destURL = fmt.Sprintf("http://%s:8080%s", c.Name, v) + } + + srcRegex, err := regexp.Compile(srcURL) + if err != nil { + return nil, errors.Wrapf(err, "invalid src regex %s", srcURL) + } + + res = append(res, discovery.UrlMapper{SrcMatch: srcRegex, Dst: destURL}) + } + return res, nil +} + +func (s *Docker) ID() string { return "docker" } + +// activate starts blocking listener for all docker events +// filters everything except "container" type, detects stop/start events and publishes signals to eventsCh +func (s *Docker) events(ctx context.Context, client DockerClient, eventsCh chan struct{}) error { + dockerEventsCh := make(chan *dclient.APIEvents) + if err := client.AddEventListener(dockerEventsCh); err != nil { + return errors.Wrap(err, "can't add even listener") + } + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case ev, ok := <-dockerEventsCh: + if !ok { + return errors.New("events closed") + } + if ev.Type != "container" { + continue + } + if !contains(ev.Status, upStatuses) && !contains(ev.Status, downStatuses) { + continue + } + log.Printf("[DEBUG] api event %+v", ev) + containerName := strings.TrimPrefix(ev.Actor.Attributes["name"], "/") + + if contains(containerName, s.Excludes) { + log.Printf("[DEBUG] container %s excluded", containerName) + continue + } + log.Printf("[INFO] new event %+v", ev) + eventsCh <- struct{}{} + } + } +} + +func (s *Docker) listContainers() (res []containerInfo, err error) { + + containers, err := s.DockerClient.ListContainers(dclient.ListContainersOptions{All: false}) + if err != nil { + return nil, errors.Wrap(err, "can't list containers") + } + log.Printf("[DEBUG] total containers = %d", len(containers)) + + for _, c := range containers { + if !contains(c.Status, upStatuses) { + continue + } + containerName := strings.TrimPrefix(c.Names[0], "/") + if contains(containerName, s.Excludes) { + log.Printf("[DEBUG] container %s excluded", containerName) + continue + } + event := containerInfo{ + Name: containerName, + ID: c.ID, + TS: time.Unix(c.Created/1000, 0), + Labels: c.Labels, + } + log.Printf("[DEBUG] running container added, %+v", event) + res = append(res, event) + } + log.Print("[DEBUG] completed list") + return res, nil +} + +func contains(e string, s []string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} diff --git a/app/discovery/provider/docker_client_mock.go b/app/discovery/provider/docker_client_mock.go new file mode 100644 index 0000000..6d1c97a --- /dev/null +++ b/app/discovery/provider/docker_client_mock.go @@ -0,0 +1,114 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package provider + +import ( + "sync" + + dclient "github.com/fsouza/go-dockerclient" +) + +// DockerClientMock is a mock implementation of DockerClient. +// +// func TestSomethingThatUsesDockerClient(t *testing.T) { +// +// // make and configure a mocked DockerClient +// mockedDockerClient := &DockerClientMock{ +// AddEventListenerFunc: func(listener chan<- *dclient.APIEvents) error { +// panic("mock out the AddEventListener method") +// }, +// ListContainersFunc: func(opts dclient.ListContainersOptions) ([]dclient.APIContainers, error) { +// panic("mock out the ListContainers method") +// }, +// } +// +// // use mockedDockerClient in code that requires DockerClient +// // and then make assertions. +// +// } +type DockerClientMock struct { + // AddEventListenerFunc mocks the AddEventListener method. + AddEventListenerFunc func(listener chan<- *dclient.APIEvents) error + + // ListContainersFunc mocks the ListContainers method. + ListContainersFunc func(opts dclient.ListContainersOptions) ([]dclient.APIContainers, error) + + // calls tracks calls to the methods. + calls struct { + // AddEventListener holds details about calls to the AddEventListener method. + AddEventListener []struct { + // Listener is the listener argument value. + Listener chan<- *dclient.APIEvents + } + // ListContainers holds details about calls to the ListContainers method. + ListContainers []struct { + // Opts is the opts argument value. + Opts dclient.ListContainersOptions + } + } + lockAddEventListener sync.RWMutex + lockListContainers sync.RWMutex +} + +// AddEventListener calls AddEventListenerFunc. +func (mock *DockerClientMock) AddEventListener(listener chan<- *dclient.APIEvents) error { + if mock.AddEventListenerFunc == nil { + panic("DockerClientMock.AddEventListenerFunc: method is nil but DockerClient.AddEventListener was just called") + } + callInfo := struct { + Listener chan<- *dclient.APIEvents + }{ + Listener: listener, + } + mock.lockAddEventListener.Lock() + mock.calls.AddEventListener = append(mock.calls.AddEventListener, callInfo) + mock.lockAddEventListener.Unlock() + return mock.AddEventListenerFunc(listener) +} + +// AddEventListenerCalls gets all the calls that were made to AddEventListener. +// Check the length with: +// len(mockedDockerClient.AddEventListenerCalls()) +func (mock *DockerClientMock) AddEventListenerCalls() []struct { + Listener chan<- *dclient.APIEvents +} { + var calls []struct { + Listener chan<- *dclient.APIEvents + } + mock.lockAddEventListener.RLock() + calls = mock.calls.AddEventListener + mock.lockAddEventListener.RUnlock() + return calls +} + +// ListContainers calls ListContainersFunc. +func (mock *DockerClientMock) ListContainers(opts dclient.ListContainersOptions) ([]dclient.APIContainers, error) { + if mock.ListContainersFunc == nil { + panic("DockerClientMock.ListContainersFunc: method is nil but DockerClient.ListContainers was just called") + } + callInfo := struct { + Opts dclient.ListContainersOptions + }{ + Opts: opts, + } + mock.lockListContainers.Lock() + mock.calls.ListContainers = append(mock.calls.ListContainers, callInfo) + mock.lockListContainers.Unlock() + return mock.ListContainersFunc(opts) +} + +// ListContainersCalls gets all the calls that were made to ListContainers. +// Check the length with: +// len(mockedDockerClient.ListContainersCalls()) +func (mock *DockerClientMock) ListContainersCalls() []struct { + Opts dclient.ListContainersOptions +} { + var calls []struct { + Opts dclient.ListContainersOptions + } + mock.lockListContainers.RLock() + calls = mock.calls.ListContainers + mock.lockListContainers.RUnlock() + return calls +} diff --git a/app/discovery/provider/docker_test.go b/app/discovery/provider/docker_test.go new file mode 100644 index 0000000..62cd1c8 --- /dev/null +++ b/app/discovery/provider/docker_test.go @@ -0,0 +1,64 @@ +package provider + +import ( + "context" + "testing" + "time" + + dclient "github.com/fsouza/go-dockerclient" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDocker_List(t *testing.T) { + dc := &DockerClientMock{ + ListContainersFunc: func(opts dclient.ListContainersOptions) ([]dclient.APIContainers, error) { + return []dclient.APIContainers{ + {Names: []string{"c1"}, Status: "start", + Labels: map[string]string{"dpx.route": "^/api/123/(.*)", "dpx.dest": "/blah/$1"}}, + {Names: []string{"c2"}, Status: "start"}, + {Names: []string{"c3"}, Status: "stop"}, + }, nil + }, + } + + d := Docker{DockerClient: dc} + res, err := d.List() + require.NoError(t, err) + assert.Equal(t, 2, len(res)) + + assert.Equal(t, "^/api/123/(.*)", res[0].SrcMatch.String()) + assert.Equal(t, "http://c1:8080/blah/$1", res[0].Dst) + + assert.Equal(t, "^/api/c2/(.*)", res[1].SrcMatch.String()) + assert.Equal(t, "http://c2:8080/$1", res[1].Dst) +} + +func TestDocker_Events(t *testing.T) { + dc := &DockerClientMock{ + AddEventListenerFunc: func(listener chan<- *dclient.APIEvents) error { + go func() { + time.Sleep(30 * time.Millisecond) + listener <- &dclient.APIEvents{Type: "container", Status: "start", + Actor: dclient.APIActor{Attributes: map[string]string{"name": "/c1"}}} + time.Sleep(30 * time.Millisecond) + listener <- &dclient.APIEvents{Type: "bad", Status: "start", + Actor: dclient.APIActor{Attributes: map[string]string{"name": "/c2"}}} + }() + return nil + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + d := Docker{DockerClient: dc} + ch := d.Events(ctx) + + events := 0 + for range ch { + t.Log("event") + events++ + } + assert.Equal(t, 1, events) +} diff --git a/app/discovery/provider/file.go b/app/discovery/provider/file.go new file mode 100644 index 0000000..4513329 --- /dev/null +++ b/app/discovery/provider/file.go @@ -0,0 +1,91 @@ +package provider + +import ( + "bufio" + "context" + "os" + "regexp" + "strings" + "time" + + log "github.com/go-pkgz/lgr" + "github.com/pkg/errors" + + "github.com/umputun/docker-proxy/app/discovery" +) + +// File implements file-based provider +// Each line contains src:dst pairs, i.e. ^/api/svc1/(.*) http://127.0.0:8080/blah/$1 +type File struct { + FileName string + CheckInterval time.Duration + Delay time.Duration +} + +// Events returns channel updating on file change only +func (d *File) Events(ctx context.Context) <-chan struct{} { + res := make(chan struct{}) + + // no need to queue multiple events or wait + trySubmit := func(ch chan struct{}) { + select { + case ch <- struct{}{}: + default: + } + } + + go func() { + tk := time.NewTicker(d.CheckInterval) + lastModif := time.Time{} + for { + select { + case <-tk.C: + fi, err := os.Stat(d.FileName) + if err != nil { + continue + } + if fi.ModTime() != lastModif { + // don't react on modification right away + if fi.ModTime().Sub(lastModif) < d.Delay { + continue + } + log.Printf("[DEBUG] file %s changed, %s -> %s", d.FileName, + lastModif.Format(time.RFC3339Nano), fi.ModTime().Format(time.RFC3339Nano)) + lastModif = fi.ModTime() + trySubmit(res) + } + case <-ctx.Done(): + close(res) + tk.Stop() + return + } + } + }() + return res +} + +// List all src dst pairs +func (d *File) List() (res []discovery.UrlMapper, err error) { + fh, err := os.Open(d.FileName) + if err != nil { + return nil, errors.Wrapf(err, "can't open %s", d.FileName) + } + defer fh.Close() + + s := bufio.NewScanner(fh) + for s.Scan() { + line := s.Text() + elems := strings.Fields(line) + if len(elems) != 2 { + continue + } + rx, err := regexp.Compile(elems[0]) + if err != nil { + return nil, errors.Wrapf(err, "can't parse regex %s", elems[0]) + } + res = append(res, discovery.UrlMapper{SrcMatch: rx, Dst: elems[1]}) + } + return res, s.Err() +} + +func (d *File) ID() string { return "file" } diff --git a/app/discovery/provider/file_test.go b/app/discovery/provider/file_test.go new file mode 100644 index 0000000..fcdbff6 --- /dev/null +++ b/app/discovery/provider/file_test.go @@ -0,0 +1,63 @@ +package provider + +import ( + "context" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFile_Events(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + tmp, err := ioutil.TempFile(os.TempDir(), "dpx-events") + require.NoError(t, err) + tmp.Close() + defer os.Remove(tmp.Name()) + + f := File{ + FileName: tmp.Name(), + CheckInterval: 10 * time.Millisecond, + Delay: 20 * time.Millisecond, + } + + go func() { + time.Sleep(30 * time.Millisecond) + assert.NoError(t, ioutil.WriteFile(tmp.Name(), []byte("something"), 0600)) + time.Sleep(30 * time.Millisecond) + assert.NoError(t, ioutil.WriteFile(tmp.Name(), []byte("something"), 0600)) + time.Sleep(30 * time.Millisecond) + assert.NoError(t, ioutil.WriteFile(tmp.Name(), []byte("something"), 0600)) + + // all those event will be ignored, submitted too fast + assert.NoError(t, ioutil.WriteFile(tmp.Name(), []byte("something"), 0600)) + assert.NoError(t, ioutil.WriteFile(tmp.Name(), []byte("something"), 0600)) + assert.NoError(t, ioutil.WriteFile(tmp.Name(), []byte("something"), 0600)) + assert.NoError(t, ioutil.WriteFile(tmp.Name(), []byte("something"), 0600)) + assert.NoError(t, ioutil.WriteFile(tmp.Name(), []byte("something"), 0600)) + }() + + ch := f.Events(ctx) + events := 0 + for range ch { + t.Log("event") + events++ + } + assert.Equal(t, 4, events) +} + +func TestFile_List(t *testing.T) { + f := File{FileName: "testdata/routes.txt"} + + res, err := f.List() + require.NoError(t, err) + t.Logf("%+v", res) + assert.Equal(t, 3, len(res)) + assert.Equal(t, "^/api/svc1/(.*)", res[0].SrcMatch.String()) + assert.Equal(t, "http://127.0.0.2:8080/blah2/$1/abc", res[1].Dst) +} diff --git a/app/discovery/provider/static.go b/app/discovery/provider/static.go new file mode 100644 index 0000000..ea09e85 --- /dev/null +++ b/app/discovery/provider/static.go @@ -0,0 +1,41 @@ +package provider + +import ( + "context" + "regexp" + "strings" + + "github.com/pkg/errors" + + "github.com/umputun/docker-proxy/app/discovery" +) + +// Static provider, rules are from::to +type Static struct { + Rules []string +} + +// Events returns channel updating on file change only +func (s *Static) Events(ctx context.Context) <-chan struct{} { + res := make(chan struct{}, 1) + res <- struct{}{} + return res +} + +// List all src dst pairs +func (s *Static) List() (res []discovery.UrlMapper, err error) { + for _, r := range s.Rules { + elems := strings.Split(r, "::") + if len(elems) != 2 { + continue + } + rx, err := regexp.Compile(elems[0]) + if err != nil { + return nil, errors.Wrapf(err, "can't parse regex %s", elems[0]) + } + res = append(res, discovery.UrlMapper{SrcMatch: rx, Dst: elems[1]}) + } + return res, nil +} + +func (s *Static) ID() string { return "static" } diff --git a/app/discovery/provider/testdata/routes.txt b/app/discovery/provider/testdata/routes.txt new file mode 100644 index 0000000..ceeaf99 --- /dev/null +++ b/app/discovery/provider/testdata/routes.txt @@ -0,0 +1,4 @@ +^/api/svc1/(.*) http://127.0.0.1:8080/blah1/$1 +^/api/svc2/(.*) http://127.0.0.2:8080/blah2/$1/abc +/api/svc3/xyz http://127.0.0.3:8080/blah3/xyz + diff --git a/app/discovery/provider_mock.go b/app/discovery/provider_mock.go new file mode 100644 index 0000000..4194a92 --- /dev/null +++ b/app/discovery/provider_mock.go @@ -0,0 +1,146 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package discovery + +import ( + "context" + "sync" +) + +// Ensure, that ProviderMock does implement Provider. +// If this is not the case, regenerate this file with moq. +var _ Provider = &ProviderMock{} + +// ProviderMock is a mock implementation of Provider. +// +// func TestSomethingThatUsesProvider(t *testing.T) { +// +// // make and configure a mocked Provider +// mockedProvider := &ProviderMock{ +// EventsFunc: func(ctx context.Context) <-chan struct{} { +// panic("mock out the Events method") +// }, +// IDFunc: func() string { +// panic("mock out the ID method") +// }, +// ListFunc: func() ([]UrlMapper, error) { +// panic("mock out the List method") +// }, +// } +// +// // use mockedProvider in code that requires Provider +// // and then make assertions. +// +// } +type ProviderMock struct { + // EventsFunc mocks the Events method. + EventsFunc func(ctx context.Context) <-chan struct{} + + // IDFunc mocks the ID method. + IDFunc func() string + + // ListFunc mocks the List method. + ListFunc func() ([]UrlMapper, error) + + // calls tracks calls to the methods. + calls struct { + // Events holds details about calls to the Events method. + Events []struct { + // Ctx is the ctx argument value. + Ctx context.Context + } + // ID holds details about calls to the ID method. + ID []struct { + } + // List holds details about calls to the List method. + List []struct { + } + } + lockEvents sync.RWMutex + lockID sync.RWMutex + lockList sync.RWMutex +} + +// Events calls EventsFunc. +func (mock *ProviderMock) Events(ctx context.Context) <-chan struct{} { + if mock.EventsFunc == nil { + panic("ProviderMock.EventsFunc: method is nil but Provider.Events was just called") + } + callInfo := struct { + Ctx context.Context + }{ + Ctx: ctx, + } + mock.lockEvents.Lock() + mock.calls.Events = append(mock.calls.Events, callInfo) + mock.lockEvents.Unlock() + return mock.EventsFunc(ctx) +} + +// EventsCalls gets all the calls that were made to Events. +// Check the length with: +// len(mockedProvider.EventsCalls()) +func (mock *ProviderMock) EventsCalls() []struct { + Ctx context.Context +} { + var calls []struct { + Ctx context.Context + } + mock.lockEvents.RLock() + calls = mock.calls.Events + mock.lockEvents.RUnlock() + return calls +} + +// ID calls IDFunc. +func (mock *ProviderMock) ID() string { + if mock.IDFunc == nil { + panic("ProviderMock.IDFunc: method is nil but Provider.ID was just called") + } + callInfo := struct { + }{} + mock.lockID.Lock() + mock.calls.ID = append(mock.calls.ID, callInfo) + mock.lockID.Unlock() + return mock.IDFunc() +} + +// IDCalls gets all the calls that were made to ID. +// Check the length with: +// len(mockedProvider.IDCalls()) +func (mock *ProviderMock) IDCalls() []struct { +} { + var calls []struct { + } + mock.lockID.RLock() + calls = mock.calls.ID + mock.lockID.RUnlock() + return calls +} + +// List calls ListFunc. +func (mock *ProviderMock) List() ([]UrlMapper, error) { + if mock.ListFunc == nil { + panic("ProviderMock.ListFunc: method is nil but Provider.List was just called") + } + callInfo := struct { + }{} + mock.lockList.Lock() + mock.calls.List = append(mock.calls.List, callInfo) + mock.lockList.Unlock() + return mock.ListFunc() +} + +// ListCalls gets all the calls that were made to List. +// Check the length with: +// len(mockedProvider.ListCalls()) +func (mock *ProviderMock) ListCalls() []struct { +} { + var calls []struct { + } + mock.lockList.RLock() + calls = mock.calls.List + mock.lockList.RUnlock() + return calls +} diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..a584f97 --- /dev/null +++ b/app/main.go @@ -0,0 +1,157 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "runtime" + "syscall" + "time" + + docker "github.com/fsouza/go-dockerclient" + "github.com/go-pkgz/lgr" + "github.com/pkg/errors" + "github.com/umputun/docker-proxy/app/discovery" + "github.com/umputun/docker-proxy/app/discovery/provider" + "github.com/umputun/docker-proxy/app/proxy" + "github.com/umputun/go-flags" +) + +var opts struct { + Listen string `short:"l" long:"listen" env:"LISTEN" default:"127.0.0.1:8080" description:"listen on host:port"` + TimeOut time.Duration `short:"t" long:"timeout" env:"TIMEOUT" default:"5s" description:"proxy timeout"` + MaxSize int64 `long:"m" long:"max" env:"MAX_SIZE" default:"64000" description:"max response size"` + GzipEnabled bool `short:"g" long:"gzip" env:"GZIP" description:"enable gz compression"` + + Assets struct { + Location string `short:"a" long:"location" env:"LOCATION" default:"" description:"assets location"` + WebRoot string `long:"root" env:"ROOT" default:"/" description:"assets web root"` + } `group:"assets" namespace:"assets" env-namespace:"ASSETS"` + + Docker struct { + Enabled bool `long:"enabled" env:"ENABLED" description:"enable docker provider"` + Host string `long:"host" env:"HOST" default:"unix:///var/run/docker.sock" description:"docker host"` + Excluded []string `long:"exclude" env:"EXCLUDE" description:"excluded containers"` + } `group:"docker" namespace:"docker" env-namespace:"DOCKER"` + + File struct { + Enabled bool `long:"enabled" env:"ENABLED" description:"enable file provider"` + Name string `long:"name" env:"NAME" default:"dpx.conf" description:"file name"` + CheckInterval time.Duration `long:"interval" env:"INTERVAL" default:"3s" description:"file check interval"` + Delay time.Duration `long:"delay" env:"DELAY" default:"500ms" description:"file event delay"` + } `group:"file" namespace:"file" env-namespace:"FILE"` + + Static struct { + Enabled bool `long:"enabled" env:"ENABLED" description:"enable file provider"` + Rules []string `long:"rule" env:"RULES" description:"routing rules"` + } `group:"static" namespace:"static" env-namespace:"STATIC"` + + Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"` +} + +var revision = "unknown" + +func main() { + fmt.Printf("docker-proxy (dpx) %s\n", revision) + + p := flags.NewParser(&opts, flags.PrintErrors|flags.PassDoubleDash|flags.HelpFlag) + p.SubcommandsOptional = true + if _, err := p.Parse(); err != nil { + if err.(*flags.Error).Type != flags.ErrHelp { + log.Printf("[ERROR] cli error: %v", err) + } + os.Exit(1) + } + + setupLog(opts.Dbg) + catchSignal() + defer func() { + if x := recover(); x != nil { + log.Printf("[WARN] run time panic:\n%v", x) + panic(x) + } + }() + + providers, err := makeProviders() + if err != nil { + log.Fatalf("[ERROR] failed to make providers, %v", err) + } + + svc := discovery.NewService(providers) + go func() { + if err := svc.Do(context.Background()); err != nil { + log.Fatalf("[ERROR] discovery failed, %v", err) + } + }() + + px := &proxy.Http{ + Version: revision, + Matcher: svc, + Address: opts.Listen, + TimeOut: opts.TimeOut, + MaxBodySize: opts.MaxSize, + AssetsLocation: opts.Assets.Location, + AssetsWebRoot: opts.Assets.WebRoot, + GzEnabled: opts.GzipEnabled, + } + if err := px.Do(context.Background()); err != nil { + log.Fatalf("[ERROR] proxy server failed, %v", err) + } +} + +func makeProviders() ([]discovery.Provider, error) { + var res []discovery.Provider + + if opts.File.Enabled { + res = append(res, &provider.File{ + FileName: opts.File.Name, + CheckInterval: opts.File.CheckInterval, + Delay: opts.File.Delay, + }) + } + + if opts.Docker.Enabled { + client, err := docker.NewClient(opts.Docker.Host) + if err != nil { + return nil, errors.Wrapf(err, "failed to make docker client %s", err) + } + res = append(res, &provider.Docker{DockerClient: client, Excludes: opts.Docker.Excluded}) + } + + if opts.Static.Enabled { + res = append(res, &provider.Static{Rules: opts.Static.Rules}) + } + + if len(res) == 0 { + return nil, errors.Errorf("no providers enabled") + } + return res, nil +} + +func setupLog(dbg bool) { + + logOpts := []lgr.Option{lgr.Msec, lgr.LevelBraces, lgr.StackTraceOnError} + if dbg { + logOpts = []lgr.Option{lgr.Debug, lgr.CallerFile, lgr.CallerFunc, lgr.Msec, lgr.LevelBraces, lgr.StackTraceOnError} + } + lgr.SetupStdLogger(logOpts...) +} + +func catchSignal() { + // catch SIGQUIT and print stack traces + sigChan := make(chan os.Signal) + go func() { + for range sigChan { + log.Print("[INFO] SIGQUIT detected") + stacktrace := make([]byte, 8192) + length := runtime.Stack(stacktrace, true) + if length > 8192 { + length = 8192 + } + fmt.Println(string(stacktrace[:length])) + } + }() + signal.Notify(sigChan, syscall.SIGQUIT) +} diff --git a/app/proxy/middleware/gzip.go b/app/proxy/middleware/gzip.go new file mode 100644 index 0000000..1f7ec04 --- /dev/null +++ b/app/proxy/middleware/gzip.go @@ -0,0 +1,48 @@ +package middleware + +import ( + "compress/gzip" + "io" + "io/ioutil" + "net/http" + "strings" + "sync" +) + +var gzPool = sync.Pool{ + New: func() interface{} { return gzip.NewWriter(ioutil.Discard) }, +} + +type gzipResponseWriter struct { + io.Writer + http.ResponseWriter +} + +func (w *gzipResponseWriter) WriteHeader(status int) { + w.Header().Del("Content-Length") + w.ResponseWriter.WriteHeader(status) +} + +func (w *gzipResponseWriter) Write(b []byte) (int, error) { + return w.Writer.Write(b) +} + +// Gzip is a middleware compressing response +func Gzip(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + next.ServeHTTP(w, r) + return + } + + w.Header().Set("Content-Encoding", "gzip") + + gz := gzPool.Get().(*gzip.Writer) + defer gzPool.Put(gz) + + gz.Reset(w) + defer gz.Close() + + next.ServeHTTP(&gzipResponseWriter{ResponseWriter: w, Writer: gz}, r) + }) +} diff --git a/app/proxy/middleware/gzip_test.go b/app/proxy/middleware/gzip_test.go new file mode 100644 index 0000000..6b5c68b --- /dev/null +++ b/app/proxy/middleware/gzip_test.go @@ -0,0 +1,62 @@ +package middleware + +import ( + "bytes" + "compress/gzip" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGzip(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("Lorem Ipsum is simply dummy text of the printing and typesetting industry. " + + "Lorem Ipsum has been the industry’s standard dummy text ever since the 1500s, when an unknown printer took " + + "a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries," + + " but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised" + + " in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, " + + "and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.")) + require.NoError(t, err) + }) + ts := httptest.NewServer(Gzip(handler)) + defer ts.Close() + + client := http.Client{} + + { + req, err := http.NewRequest("GET", ts.URL+"/something", nil) + require.NoError(t, err) + req.Header.Set("Accept-Encoding", "gzip") + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) + defer resp.Body.Close() + b, err := ioutil.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Equal(t, 355, len(b), "compressed size") + + gzr, err := gzip.NewReader(bytes.NewBuffer(b)) + require.NoError(t, err) + b, err = ioutil.ReadAll(gzr) + require.NoError(t, err) + assert.True(t, strings.HasPrefix(string(b), "Lorem Ipsum"), string(b)) + } + { + req, err := http.NewRequest("GET", ts.URL+"/something", nil) + require.NoError(t, err) + resp, err := client.Do(req) + require.Nil(t, err) + assert.Equal(t, 200, resp.StatusCode) + defer resp.Body.Close() + b, err := ioutil.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Equal(t, 576, len(b), "uncompressed size") + + } + +} diff --git a/app/proxy/proxy.go b/app/proxy/proxy.go new file mode 100644 index 0000000..48efb38 --- /dev/null +++ b/app/proxy/proxy.go @@ -0,0 +1,133 @@ +package proxy + +import ( + "context" + "log" + "net/http" + "net/http/httputil" + "net/url" + "time" + + "github.com/go-pkgz/rest" + "github.com/go-pkgz/rest/logger" + "github.com/umputun/docker-proxy/app/proxy/middleware" +) + +type Http struct { + Matcher + Address string + TimeOut time.Duration + AssetsLocation string + AssetsWebRoot string + MaxBodySize int64 + GzEnabled bool + Version string +} + +type Matcher interface { + Match(url string) (string, bool) +} + +func (h *Http) Do(ctx context.Context) error { + log.Printf("[INFO] run proxy on %s", h.Address) + if h.AssetsLocation != "" { + log.Printf("[DEBUG] assets file server enabled for %s", h.AssetsLocation) + } + + httpServer := &http.Server{ + Addr: h.Address, + Handler: h.wrap(h.proxyHandler(), + rest.AppInfo("dpx", "umputun", h.Version), + rest.Ping, + logger.New(logger.Prefix("[DEBUG] PROXY")).Handler, + rest.SizeLimit(h.MaxBodySize), + h.gzipHandler(), + ), + ReadHeaderTimeout: 5 * time.Second, + WriteTimeout: 120 * time.Second, + IdleTimeout: 30 * time.Second, + } + + go func() { + <-ctx.Done() + if err := httpServer.Close(); err != nil { + log.Printf("[ERROR] failed to close proxy server, %v", err) + } + }() + + return httpServer.ListenAndServe() +} + +func (h *Http) gzipHandler() func(next http.Handler) http.Handler { + gzHandler := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + }) + } + if h.GzEnabled { + gzHandler = middleware.Gzip + } + return gzHandler +} + +// wrap convert a list of middlewares to nested calls, in reversed order +func (h *Http) wrap(p http.Handler, mws ...func(http.Handler) http.Handler) http.Handler { + res := p + for i := len(mws) - 1; i >= 0; i-- { + res = mws[i](res) + } + return res +} + +func (h *Http) proxyHandler() http.HandlerFunc { + type contextKey string + + reverseProxy := &httputil.ReverseProxy{ + Director: func(r *http.Request) { + ctx := r.Context() + uu := ctx.Value(contextKey("url")).(*url.URL) + r.URL.Path = uu.Path + r.URL.Host = uu.Host + r.URL.Scheme = uu.Scheme + r.Header.Add("X-Forwarded-Host", uu.Host) + r.Header.Add("X-Origin-Host", r.Host) + }, + } + + reverseProxy.Transport = http.DefaultTransport + reverseProxy.Transport.(*http.Transport).ResponseHeaderTimeout = h.TimeOut + + // default assetsHandler disabled, returns error on missing matches + assetsHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Printf("[WARN] mo match for %s", r.URL) + http.Error(w, "Server error", http.StatusBadGateway) + }) + + if h.AssetsLocation != "" && h.AssetsWebRoot != "" { + fs, err := rest.FileServer(h.AssetsWebRoot, h.AssetsLocation) + if err == nil { + assetsHandler = func(w http.ResponseWriter, r *http.Request) { + fs.ServeHTTP(w, r) + } + } + + } + + return func(w http.ResponseWriter, r *http.Request) { + + u, ok := h.Match(r.URL.Path) + if !ok { + assetsHandler.ServeHTTP(w, r) + return + } + + uu, err := url.Parse(u) + if err != nil { + http.Error(w, "Server error", http.StatusBadGateway) + return + } + + ctx := context.WithValue(r.Context(), contextKey("url"), uu) // set destination url in request's context + reverseProxy.ServeHTTP(w, r.WithContext(ctx)) + } +} diff --git a/app/proxy/proxy_test.go b/app/proxy/proxy_test.go new file mode 100644 index 0000000..e8acf92 --- /dev/null +++ b/app/proxy/proxy_test.go @@ -0,0 +1,60 @@ +package proxy + +import ( + "context" + "fmt" + "io" + "math/rand" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/umputun/docker-proxy/app/discovery" + "github.com/umputun/docker-proxy/app/discovery/provider" +) + +func TestHttp_Do(t *testing.T) { + port := rand.Intn(10000) + 40000 + h := Http{TimeOut: 200 * time.Millisecond, Address: fmt.Sprintf("127.0.0.1:%d", port)} + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + ds := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("%v", r) + w.Header().Add("h1", "v1") + w.Write([]byte("response")) + })) + + svc := discovery.NewService([]discovery.Provider{ + &provider.Static{Rules: []string{"^/api/(.*)::" + ds.URL + "/123/$1"}}}) + + go func() { + svc.Do(context.Background()) + }() + + h.Matcher = svc + go func() { + h.Do(ctx) + }() + time.Sleep(10 * time.Millisecond) + + client := http.Client{} + resp, err := client.Get("http://127.0.0.1:" + strconv.Itoa(port) + "/api/something") + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + t.Logf("%+v", resp.Header) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, "response", string(body)) + assert.Equal(t, "dpx", resp.Header.Get("App-Name")) + assert.Equal(t, "v1", resp.Header.Get("h1")) + + resp, err = client.Get("http://127.0.0.1:" + strconv.Itoa(port) + "/bad/something") + require.NoError(t, err) + assert.Equal(t, http.StatusBadGateway, resp.StatusCode) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a9b3c67 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module github.com/umputun/docker-proxy + +go 1.16 + +require ( + github.com/fsouza/go-dockerclient v1.7.2 + github.com/go-pkgz/lgr v0.10.4 + github.com/go-pkgz/rest v1.7.0 + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.6.1 + github.com/umputun/go-flags v1.5.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..75064e7 --- /dev/null +++ b/go.sum @@ -0,0 +1,206 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/hcsshim v0.8.14 h1:lbPVK25c1cu5xTLITwpUcxoA9vKrKErASPYygvouJns= +github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59 h1:qWj4qVYZ95vLWwqyNJCQg7rDsG5wPdze0UaPolH7DUk= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.3 h1:ijQT13JedHSHrQGWFcGEwzcNKrAGIiZ+jSD5QQG07SY= +github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e h1:6JKvHHt396/qabvMhnhUZvWaHZzfVfldxE60TK8YLhg= +github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/docker/docker v20.10.3-0.20210216175712-646072ed6524+incompatible h1:Yu2uGErhwEoOT/OxAFe+/SiJCqRLs+pgcS5XKrDXnG4= +github.com/docker/docker v20.10.3-0.20210216175712-646072ed6524+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/fsouza/go-dockerclient v1.7.2 h1:bBEAcqLTkpq205jooP5RVroUKiVEWgGecHyeZc4OFjo= +github.com/fsouza/go-dockerclient v1.7.2/go.mod h1:+ugtMCVRwnPfY7d8/baCzZ3uwB0BrG5DB8OzbtxaRz8= +github.com/go-pkgz/lgr v0.10.4 h1:l7qyFjqEZgwRgaQQSEp6tve4A3OU80VrfzpvtEX8ngw= +github.com/go-pkgz/lgr v0.10.4/go.mod h1:CD0s1z6EFpIUplV067gitF77tn25JItzwHNKAPqeCF0= +github.com/go-pkgz/rest v1.7.0 h1:5KHWmYPZaJfd6+Htx8bCJ2InVSHJ85MGh8kuO72SAjU= +github.com/go-pkgz/rest v1.7.0/go.mod h1:FKpgK5FgSqREG323OIU/JpIc0xA7dqay9BmK7LZXTQE= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +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 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +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.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +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/moby/sys/mount v0.2.0 h1:WhCW5B355jtxndN5ovugJlMFJawbUODuW8fSnEH6SSM= +github.com/moby/sys/mount v0.2.0/go.mod h1:aAivFE2LB3W4bACsUXChRHQ0qKWsetY4Y9V7sxOougM= +github.com/moby/sys/mountinfo v0.4.0 h1:1KInV3Huv18akCu58V7lzNlt+jFmqlu1EaErnEHE/VM= +github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +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.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/umputun/go-flags v1.5.1 h1:vRauoXV3Ultt1HrxivSxowbintgZLJE+EcBy5ta3/mY= +github.com/umputun/go-flags v1.5.1/go.mod h1:nTbvsO/hKqe7Utri/NoyN18GR3+EWf+9RrmsdwdhrEc= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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/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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/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-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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +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-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/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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-20190215142949-d0b11bdaac8a/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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210216224549-f992740a1bac h1:9glrpwtNjBYgRpb67AZJKHfzj1stG/8BL5H7In2oTC4= +golang.org/x/sys v0.0.0-20210216224549-f992740a1bac/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201113234701-d7a72108b828 h1:htWEtQEuEVJ4tU/Ngx7Cd/4Q7e3A5Up1owgyBtVsTwk= +golang.org/x/term v0.0.0-20201113234701-d7a72108b828/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +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/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +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-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +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=