From 7139c57766d948fc8394973db506e21cef66e73d Mon Sep 17 00:00:00 2001 From: Umputun Date: Tue, 1 Jun 2021 02:56:39 -0500 Subject: [PATCH] RPC plugins support (#85) * wip * resolve merge artifacts * full coverage for conductor * wire plugin conductor to main and proxy * wip, with separate match handler * split matching logic with another handler, add initial docs * move parts of proxy to handlers, add tests * add headers in to be sent to proxied url * merged from master * add example with docker compose * supress excesive debug reporting 0-9 disabled in docker * add plugin tests * randomize test port * lint: minor warns * lint: err shadow --- Makefile | 2 +- README.md | 11 + app/discovery/discovery.go | 5 +- app/discovery/discovery_test.go | 40 +- app/discovery/provider/docker.go | 1 - app/main.go | 88 ++-- app/main_test.go | 114 ++++- app/plugin/client_mock.go | 85 ++++ app/plugin/conductor.go | 232 ++++++++++ app/plugin/conductor_test.go | 422 ++++++++++++++++++ app/plugin/dialer_mock.go | 79 ++++ app/proxy/handlers.go | 117 +++++ app/proxy/handlers_test.go | 85 ++++ app/proxy/matcher_mock.go | 189 ++++++++ app/proxy/proxy.go | 244 ++++------ app/proxy/proxy_test.go | 64 ++- examples/plugin/Dockerfile | 19 + examples/plugin/README.md | 2 + examples/plugin/docker-compose.yml | 44 ++ examples/plugin/go.mod | 7 + examples/plugin/go.sum | 410 +++++++++++++++++ examples/plugin/main.go | 50 +++ .../vendor/github.com/go-pkgz/lgr/.gitignore | 13 + .../github.com/go-pkgz/lgr/.golangci.yml | 70 +++ .../vendor/github.com/go-pkgz/lgr/LICENSE | 21 + .../vendor/github.com/go-pkgz/lgr/README.md | 124 +++++ .../vendor/github.com/go-pkgz/lgr/adaptor.go | 41 ++ .../vendor/github.com/go-pkgz/lgr/go.mod | 5 + .../vendor/github.com/go-pkgz/lgr/go.sum | 12 + .../github.com/go-pkgz/lgr/interface.go | 48 ++ .../vendor/github.com/go-pkgz/lgr/logger.go | 407 +++++++++++++++++ .../vendor/github.com/go-pkgz/lgr/mapper.go | 28 ++ .../vendor/github.com/go-pkgz/lgr/options.go | 92 ++++ .../github.com/go-pkgz/repeater/.gitignore | 12 + .../github.com/go-pkgz/repeater/.golangci.yml | 60 +++ .../github.com/go-pkgz/repeater/.travis.yml | 20 + .../github.com/go-pkgz/repeater/LICENSE | 21 + .../github.com/go-pkgz/repeater/README.md | 35 ++ .../vendor/github.com/go-pkgz/repeater/go.mod | 5 + .../vendor/github.com/go-pkgz/repeater/go.sum | 7 + .../github.com/go-pkgz/repeater/repeater.go | 64 +++ .../go-pkgz/repeater/strategy/backoff.go | 59 +++ .../go-pkgz/repeater/strategy/fixed.go | 36 ++ .../go-pkgz/repeater/strategy/strategy.go | 35 ++ .../vendor/github.com/umputun/reproxy/LICENSE | 21 + .../github.com/umputun/reproxy/lib/plugin.go | 105 +++++ .../github.com/umputun/reproxy/lib/rpc.go | 31 ++ examples/plugin/vendor/modules.txt | 9 + go.mod | 1 + go.sum | 2 + lib/plugin.go | 112 +++++ lib/plugin_test.go | 85 ++++ lib/rpc.go | 31 ++ vendor/github.com/go-pkgz/repeater/.gitignore | 12 + .../github.com/go-pkgz/repeater/.golangci.yml | 60 +++ .../github.com/go-pkgz/repeater/.travis.yml | 20 + vendor/github.com/go-pkgz/repeater/LICENSE | 21 + vendor/github.com/go-pkgz/repeater/README.md | 35 ++ vendor/github.com/go-pkgz/repeater/go.mod | 5 + vendor/github.com/go-pkgz/repeater/go.sum | 7 + .../github.com/go-pkgz/repeater/repeater.go | 64 +++ .../go-pkgz/repeater/strategy/backoff.go | 59 +++ .../go-pkgz/repeater/strategy/fixed.go | 36 ++ .../go-pkgz/repeater/strategy/strategy.go | 35 ++ vendor/modules.txt | 4 + 65 files changed, 4053 insertions(+), 227 deletions(-) create mode 100644 app/plugin/client_mock.go create mode 100644 app/plugin/conductor.go create mode 100644 app/plugin/conductor_test.go create mode 100644 app/plugin/dialer_mock.go create mode 100644 app/proxy/handlers.go create mode 100644 app/proxy/handlers_test.go create mode 100644 app/proxy/matcher_mock.go create mode 100644 examples/plugin/Dockerfile create mode 100644 examples/plugin/README.md create mode 100644 examples/plugin/docker-compose.yml create mode 100644 examples/plugin/go.mod create mode 100644 examples/plugin/go.sum create mode 100644 examples/plugin/main.go create mode 100644 examples/plugin/vendor/github.com/go-pkgz/lgr/.gitignore create mode 100644 examples/plugin/vendor/github.com/go-pkgz/lgr/.golangci.yml create mode 100644 examples/plugin/vendor/github.com/go-pkgz/lgr/LICENSE create mode 100644 examples/plugin/vendor/github.com/go-pkgz/lgr/README.md create mode 100644 examples/plugin/vendor/github.com/go-pkgz/lgr/adaptor.go create mode 100644 examples/plugin/vendor/github.com/go-pkgz/lgr/go.mod create mode 100644 examples/plugin/vendor/github.com/go-pkgz/lgr/go.sum create mode 100644 examples/plugin/vendor/github.com/go-pkgz/lgr/interface.go create mode 100644 examples/plugin/vendor/github.com/go-pkgz/lgr/logger.go create mode 100644 examples/plugin/vendor/github.com/go-pkgz/lgr/mapper.go create mode 100644 examples/plugin/vendor/github.com/go-pkgz/lgr/options.go create mode 100644 examples/plugin/vendor/github.com/go-pkgz/repeater/.gitignore create mode 100644 examples/plugin/vendor/github.com/go-pkgz/repeater/.golangci.yml create mode 100644 examples/plugin/vendor/github.com/go-pkgz/repeater/.travis.yml create mode 100644 examples/plugin/vendor/github.com/go-pkgz/repeater/LICENSE create mode 100644 examples/plugin/vendor/github.com/go-pkgz/repeater/README.md create mode 100644 examples/plugin/vendor/github.com/go-pkgz/repeater/go.mod create mode 100644 examples/plugin/vendor/github.com/go-pkgz/repeater/go.sum create mode 100644 examples/plugin/vendor/github.com/go-pkgz/repeater/repeater.go create mode 100644 examples/plugin/vendor/github.com/go-pkgz/repeater/strategy/backoff.go create mode 100644 examples/plugin/vendor/github.com/go-pkgz/repeater/strategy/fixed.go create mode 100644 examples/plugin/vendor/github.com/go-pkgz/repeater/strategy/strategy.go create mode 100644 examples/plugin/vendor/github.com/umputun/reproxy/LICENSE create mode 100644 examples/plugin/vendor/github.com/umputun/reproxy/lib/plugin.go create mode 100644 examples/plugin/vendor/github.com/umputun/reproxy/lib/rpc.go create mode 100644 examples/plugin/vendor/modules.txt create mode 100644 lib/plugin.go create mode 100644 lib/plugin_test.go create mode 100644 lib/rpc.go create mode 100644 vendor/github.com/go-pkgz/repeater/.gitignore create mode 100644 vendor/github.com/go-pkgz/repeater/.golangci.yml create mode 100644 vendor/github.com/go-pkgz/repeater/.travis.yml create mode 100644 vendor/github.com/go-pkgz/repeater/LICENSE create mode 100644 vendor/github.com/go-pkgz/repeater/README.md create mode 100644 vendor/github.com/go-pkgz/repeater/go.mod create mode 100644 vendor/github.com/go-pkgz/repeater/go.sum create mode 100644 vendor/github.com/go-pkgz/repeater/repeater.go create mode 100644 vendor/github.com/go-pkgz/repeater/strategy/backoff.go create mode 100644 vendor/github.com/go-pkgz/repeater/strategy/fixed.go create mode 100644 vendor/github.com/go-pkgz/repeater/strategy/strategy.go diff --git a/Makefile b/Makefile index 3b2947c..a58b7ed 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ GITREV=$(shell git describe --abbrev=7 --always --tags) REV=$(GITREV)-$(BRANCH)-$(shell date +%Y%m%d-%H:%M:%S) docker: - docker build -t umputun/reproxy:master . + docker build -t umputun/reproxy:master --progress=plain . dist: - @mkdir -p dist diff --git a/README.md b/README.md index 8c7ca2b..eff07fd 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,17 @@ _see also [examples/metrics](https://github.com/umputun/reproxy/tree/master/exam Reproxy returns 502 (Bad Gateway) error in case if request doesn't match to any provided routes and assets. In case if some unexpected, internal error happened it returns 500. By default reproxy renders the simplest text version of the error - "Server error". Setting `--error.enabled` turns on the default html error message and with `--error.template` user may set any custom html template file for the error rendering. The template has two vars: `{{.ErrCode}}` and `{{.ErrMessage}}`. For example this template `oh my! {{.ErrCode}} - {{.ErrMessage}}` will be rendered to `oh my! 502 - Bad Gateway` +## Plugins support + +The core functionality of reproxy can be extended with external plugins. Each plugin is an independent process/container implementing [rpc server](https://golang.org/pkg/net/rpc/). Plugins registered with reproxy conductor and added to the chain of the middlewares. Each plugin receives request with the original url, headers and all matching route info and responds with the headers and the status code. Any status code >= 400 treated as an error response and terminates flow immediately with the proxy error. There are two types of headers plugins can set: + +- `HeadersIn` - incoming headers. Those will be sent to the proxied url +- `HeadersOut` - outgoing headers. Will be sent back to the client + +To simplify the development process all the building blocks provided. It includes `lib.Plugin` handling registration, listening and dispatching calls as well as `lib.Request` and `lib.Response` defining input and output. Plugin's authors should implement concrete handlers satisfying `func(req lib.Request, res *lib.HandlerResponse) (err error)` signature. Each plugin may contain multiple handlers like this. + + +_See [examples/plugin]() for more info_ ## Options diff --git a/app/discovery/discovery.go b/app/discovery/discovery.go index e3bfbfa..4ccca6b 100644 --- a/app/discovery/discovery.go +++ b/app/discovery/discovery.go @@ -52,6 +52,7 @@ type Matches struct { type MatchedRoute struct { Destination string Alive bool + Mapper URLMapper } // Provider defines sources of mappers @@ -159,12 +160,12 @@ func (s *Service) Match(srv, src string) (res Matches) { if src != dest { // regex matched lastSrcMatch = m.SrcMatch.String() res.MatchType = MTProxy - res.Routes = append(res.Routes, MatchedRoute{dest, m.IsAlive()}) + res.Routes = append(res.Routes, MatchedRoute{Destination: dest, Alive: m.IsAlive(), Mapper: m}) } case MTStatic: if src == m.AssetsWebRoot || strings.HasPrefix(src, m.AssetsWebRoot+"/") { res.MatchType = MTStatic - res.Routes = append(res.Routes, MatchedRoute{m.AssetsWebRoot + ":" + m.AssetsLocation, true}) + res.Routes = append(res.Routes, MatchedRoute{Destination: m.AssetsWebRoot + ":" + m.AssetsLocation, Alive: true}) return res } } diff --git a/app/discovery/discovery_test.go b/app/discovery/discovery_test.go index 66a3463..c53197d 100644 --- a/app/discovery/discovery_test.go +++ b/app/discovery/discovery_test.go @@ -126,33 +126,43 @@ func TestService_Match(t *testing.T) { server, src string res Matches }{ - {"example.com", "/api/svc3/xyz/something", Matches{MTProxy, []MatchedRoute{{"http://127.0.0.3:8080/blah3/xyz/something", true}}}}, - {"example.com", "/api/svc3/xyz", Matches{MTProxy, []MatchedRoute{{"http://127.0.0.3:8080/blah3/xyz", true}}}}, - {"abc.example.com", "/api/svc1/1234", Matches{MTProxy, []MatchedRoute{{"http://127.0.0.1:8080/blah1/1234", true}}}}, + {"example.com", "/api/svc3/xyz/something", Matches{MTProxy, []MatchedRoute{ + {Destination: "http://127.0.0.3:8080/blah3/xyz/something", Alive: true}}}}, + {"example.com", "/api/svc3/xyz", Matches{MTProxy, []MatchedRoute{{ + Destination: "http://127.0.0.3:8080/blah3/xyz", Alive: true}}}}, + {"abc.example.com", "/api/svc1/1234", Matches{MTProxy, []MatchedRoute{ + {Destination: "http://127.0.0.1:8080/blah1/1234", Alive: true}}}}, {"zzz.example.com", "/aaa/api/svc1/1234", Matches{MTProxy, nil}}, - {"m.example.com", "/api/svc2/1234", Matches{MTProxy, []MatchedRoute{{"http://127.0.0.2:8080/blah2/1234/abc", true}}}}, + {"m.example.com", "/api/svc2/1234", Matches{MTProxy, []MatchedRoute{ + {Destination: "http://127.0.0.2:8080/blah2/1234/abc", Alive: true}}}}, {"m1.example.com", "/api/svc2/1234", Matches{MTProxy, nil}}, - {"m.example.com", "/api/svc4/id12345", Matches{MTProxy, []MatchedRoute{{"http://127.0.0.4:8080/blah2/id12345/abc", false}}}}, + {"m.example.com", "/api/svc4/id12345", Matches{MTProxy, []MatchedRoute{ + {Destination: "http://127.0.0.4:8080/blah2/id12345/abc", Alive: false}}}}, {"m.example.com", "/api/svc5/num123456", Matches{MTProxy, []MatchedRoute{ - {"http://127.0.0.5:8080/blah2/num123456/abc", true}, - {"http://127.0.0.5:8080/blah2/num123456/abc/2", true}, - {"http://127.0.0.5:8080/blah2/num123456/abc/3", false}, + {Destination: "http://127.0.0.5:8080/blah2/num123456/abc", Alive: true}, + {Destination: "http://127.0.0.5:8080/blah2/num123456/abc/2", Alive: true}, + {Destination: "http://127.0.0.5:8080/blah2/num123456/abc/3", Alive: false}, }}}, - {"m1.example.com", "/web/index.html", Matches{MTStatic, []MatchedRoute{{"/web:/var/web/", true}}}}, - {"m1.example.com", "/web/", Matches{MTStatic, []MatchedRoute{{"/web:/var/web/", true}}}}, - {"m1.example.com", "/www/something", Matches{MTStatic, []MatchedRoute{{"/www:/var/web/", true}}}}, - {"m1.example.com", "/www/", Matches{MTStatic, []MatchedRoute{{"/www:/var/web/", true}}}}, - {"m1.example.com", "/www", Matches{MTStatic, []MatchedRoute{{"/www:/var/web/", true}}}}, - {"xyx.example.com", "/path/something", Matches{MTStatic, []MatchedRoute{{"/path:/var/web/path/", true}}}}, + {"m1.example.com", "/web/index.html", Matches{MTStatic, []MatchedRoute{{Destination: "/web:/var/web/", Alive: true}}}}, + {"m1.example.com", "/web/", Matches{MTStatic, []MatchedRoute{{Destination: "/web:/var/web/", Alive: true}}}}, + {"m1.example.com", "/www/something", Matches{MTStatic, []MatchedRoute{{Destination: "/www:/var/web/", Alive: true}}}}, + {"m1.example.com", "/www/", Matches{MTStatic, []MatchedRoute{{Destination: "/www:/var/web/", Alive: true}}}}, + {"m1.example.com", "/www", Matches{MTStatic, []MatchedRoute{{Destination: "/www:/var/web/", Alive: true}}}}, + {"xyx.example.com", "/path/something", Matches{MTStatic, []MatchedRoute{{Destination: "/path:/var/web/path/", Alive: true}}}}, } for i, tt := range tbl { tt := tt t.Run(strconv.Itoa(i), func(t *testing.T) { res := svc.Match(tt.server, tt.src) - assert.Equal(t, tt.res, res) + require.Equal(t, len(tt.res.Routes), len(res.Routes)) + for i := 0; i < len(res.Routes); i++ { + assert.Equal(t, tt.res.Routes[i].Alive, res.Routes[i].Alive) + assert.Equal(t, tt.res.Routes[i].Destination, res.Routes[i].Destination) + } + assert.Equal(t, tt.res.MatchType, res.MatchType) }) } } diff --git a/app/discovery/provider/docker.go b/app/discovery/provider/docker.go index f86cc93..4fa40a4 100644 --- a/app/discovery/provider/docker.go +++ b/app/discovery/provider/docker.go @@ -155,7 +155,6 @@ func (d *Docker) parseContainerInfo(c containerInfo) (res []discovery.URLMapper) } if !enabled { - log.Printf("[DEBUG] container %s (route: %d) disabled", c.Name, n) continue } diff --git a/app/main.go b/app/main.go index c680945..4b49612 100644 --- a/app/main.go +++ b/app/main.go @@ -9,6 +9,7 @@ import ( "math" "math/rand" "net/http" + "net/rpc" "os" "os/signal" "strconv" @@ -24,6 +25,7 @@ import ( "github.com/umputun/reproxy/app/discovery/provider" "github.com/umputun/reproxy/app/discovery/provider/consulcatalog" "github.com/umputun/reproxy/app/mgmt" + "github.com/umputun/reproxy/app/plugin" "github.com/umputun/reproxy/app/proxy" ) @@ -112,6 +114,11 @@ var opts struct { Interval time.Duration `long:"interval" env:"INTERVAL" default:"300s" description:"automatic health-check interval"` } `group:"health-check" namespace:"health-check" env-namespace:"HEALTH_CHECK"` + Plugin struct { + Enabled bool `long:"enabled" env:"ENABLED" description:"enable plugin support"` + Listen string `long:"listen" env:"LISTEN" default:"127.0.0.1:8081" description:"registration listen on host:port"` + } `group:"plugin" namespace:"plugin" env-namespace:"PLUGIN"` + Signature bool `long:"signature" env:"SIGNATURE" description:"enable reproxy signature headers"` Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"` } @@ -190,25 +197,6 @@ func run() error { } }() - var metrics *mgmt.Metrics // disabled by default - if opts.Management.Enabled { - metrics = mgmt.NewMetrics() - go func() { - mgSrv := mgmt.Server{ - Listen: opts.Management.Listen, - Informer: svc, - AssetsLocation: opts.Assets.Location, - AssetsWebRoot: opts.Assets.WebRoot, - Version: revision, - } - if opts.Management.Enabled { - if mgErr := mgSrv.Run(ctx); err != nil { - log.Printf("[WARN] management service failed, %v", mgErr) - } - } - }() - } - cacheControl, err := proxy.MakeCacheControl(opts.Assets.CacheControl) if err != nil { return fmt.Errorf("failed to make cache control: %w", err) @@ -253,8 +241,9 @@ func run() error { ExpectContinue: opts.Timeouts.ExpectContinue, ResponseHeader: opts.Timeouts.ResponseHeader, }, - Metrics: metrics, - Reporter: errReporter, + Metrics: makeMetrics(ctx, svc), + Reporter: errReporter, + PluginConductor: makePluginConductor(ctx), } err = px.Run(ctx) @@ -311,16 +300,43 @@ func makeProviders() ([]discovery.Provider, error) { return res, nil } -func makeLBSelector() func(len int) int { - switch opts.LBType { - case "random": - rand.Seed(time.Now().UnixNano()) - return rand.Intn - case "failover": - return func(int) int { return 0 } // dead server won't be in the list, we can safely pick the first one - default: - return func(int) int { return 0 } +func makePluginConductor(ctx context.Context) proxy.MiddlewareProvider { + if !opts.Plugin.Enabled { + return nil } + + conductor := &plugin.Conductor{ + Address: opts.Plugin.Listen, + RPCDialer: plugin.RPCDialerFunc(func(network, address string) (plugin.RPCClient, error) { + return rpc.Dial("tcp", address) + }), + } + go func() { + if err := conductor.Run(ctx); err != nil { + log.Printf("[WARN] plugin conductor error, %v", err) + } + }() + return conductor +} + +func makeMetrics(ctx context.Context, informer mgmt.Informer) proxy.MiddlewareProvider { + if !opts.Management.Enabled { + return nil + } + metrics := mgmt.NewMetrics() + go func() { + mgSrv := mgmt.Server{ + Listen: opts.Management.Listen, + Informer: informer, + AssetsLocation: opts.Assets.Location, + AssetsWebRoot: opts.Assets.WebRoot, + Version: revision, + } + if err := mgSrv.Run(ctx); err != nil { + log.Printf("[WARN] management service failed, %v", err) + } + }() + return metrics } func makeSSLConfig() (config proxy.SSLConfig, err error) { @@ -348,6 +364,18 @@ func makeSSLConfig() (config proxy.SSLConfig, err error) { return config, err } +func makeLBSelector() func(len int) int { + switch opts.LBType { + case "random": + rand.Seed(time.Now().UnixNano()) + return rand.Intn + case "failover": + return func(int) int { return 0 } // dead server won't be in the list, we can safely pick the first one + default: + return func(int) int { return 0 } + } +} + func makeErrorReporter() (proxy.Reporter, error) { result := &proxy.ErrorReporter{ Nice: opts.ErrorReport.Enabled, diff --git a/app/main_test.go b/app/main_test.go index 2d418be..287daae 100644 --- a/app/main_test.go +++ b/app/main_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/tls" "fmt" "io/ioutil" @@ -12,8 +13,11 @@ import ( "testing" "time" + log "github.com/go-pkgz/lgr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/umputun/reproxy/lib" ) func Test_Main(t *testing.T) { @@ -151,15 +155,70 @@ func Test_MainWithSSL(t *testing.T) { } } -func waitForHTTPServerStart(port int) { - // wait for up to 10 seconds for server to start before returning it - client := http.Client{Timeout: time.Second} - for i := 0; i < 100; i++ { - time.Sleep(time.Millisecond * 100) - if resp, err := client.Get(fmt.Sprintf("http://localhost:%d", port)); err == nil { - _ = resp.Body.Close() - return +func Test_MainWithPlugin(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + proxyPort := rand.Intn(10000) + 40000 + conductorPort := rand.Intn(10000) + 40000 + os.Args = []string{"test", "--static.enabled", + "--static.rule=*,/svc1, https://httpbin.org/get,https://feedmaster.umputun.com/ping", + "--static.rule=*,/svc2/(.*), https://echo.umputun.com/$1,https://feedmaster.umputun.com/ping", + "--file.enabled", "--file.name=discovery/provider/testdata/config.yml", + "--dbg", "--logger.enabled", "--logger.stdout", "--logger.file=/tmp/reproxy.log", + "--listen=127.0.0.1:" + strconv.Itoa(proxyPort), "--signature", "--error.enabled", + "--header=hh1:vv1", + "--plugin.enabled", "--plugin.listen=127.0.0.1:" + strconv.Itoa(conductorPort), + } + defer os.Remove("/tmp/reproxy.log") + done := make(chan struct{}) + go func() { + <-done + e := syscall.Kill(syscall.Getpid(), syscall.SIGTERM) + require.NoError(t, e) + }() + + finished := make(chan struct{}) + go func() { + main() + close(finished) + }() + + // defer cleanup because require check below can fail + defer func() { + close(done) + <-finished + }() + + waitForHTTPServerStart(proxyPort) + + pluginPort := rand.Intn(10000) + 40000 + plugin := lib.Plugin{Name: "TestPlugin", Address: "127.0.0.1:" + strconv.Itoa(pluginPort), Methods: []string{"HeaderThing", "ErrorThing"}} + go func() { + if err := plugin.Do(context.Background(), fmt.Sprintf("http://127.0.0.1:%d", conductorPort), &TestPlugin{}); err != nil { + require.NotEqual(t, "proxy server closed, http: Server closed", err.Error()) } + }() + + time.Sleep(time.Second) + + client := http.Client{Timeout: 10 * time.Second} + { + resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/svc1", proxyPort)) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, 200, resp.StatusCode) + body, err := ioutil.ReadAll(resp.Body) + assert.NoError(t, err) + t.Logf("body: %s", string(body)) + assert.Contains(t, string(body), `"Host": "httpbin.org"`) + assert.Contains(t, string(body), `"Inh": "val"`) + assert.Equal(t, "val1", resp.Header.Get("key1")) + assert.Equal(t, "val2", resp.Header.Get("key2")) + } + { + resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/fail", proxyPort)) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, 500, resp.StatusCode) } } @@ -254,5 +313,42 @@ func Test_sizeParse(t *testing.T) { assert.Equal(t, tt.res, res) }) } - +} + +func waitForHTTPServerStart(port int) { + // wait for up to 10 seconds for server to start before returning it + client := http.Client{Timeout: time.Second} + for i := 0; i < 100; i++ { + time.Sleep(time.Millisecond * 100) + if resp, err := client.Get(fmt.Sprintf("http://localhost:%d/ping", port)); err == nil { + _ = resp.Body.Close() + return + } + } +} + +type TestPlugin struct{} + +//nolint +func (h *TestPlugin) HeaderThing(req *lib.Request, res *lib.Response) (err error) { + log.Printf("req: %+v", req) + res.HeadersIn = http.Header{} + res.HeadersIn.Add("inh", "val") + res.HeadersOut = req.Header + res.HeadersOut.Add("key1", "val1") + res.StatusCode = 200 + return nil +} + +//nolint +func (h *TestPlugin) ErrorThing(req lib.Request, res *lib.Response) (err error) { + log.Printf("req: %+v", req) + if req.URL == "/fail" { + res.StatusCode = 500 + return nil + } + res.HeadersOut = req.Header + res.HeadersOut.Add("key2", "val2") + res.StatusCode = 200 + return nil } diff --git a/app/plugin/client_mock.go b/app/plugin/client_mock.go new file mode 100644 index 0000000..b6738cf --- /dev/null +++ b/app/plugin/client_mock.go @@ -0,0 +1,85 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package plugin + +import ( + "sync" +) + +// Ensure, that RPCClientMock does implement RPCClient. +// If this is not the case, regenerate this file with moq. +var _ RPCClient = &RPCClientMock{} + +// RPCClientMock is a mock implementation of RPCClient. +// +// func TestSomethingThatUsesRPCClient(t *testing.T) { +// +// // make and configure a mocked RPCClient +// mockedRPCClient := &RPCClientMock{ +// CallFunc: func(serviceMethod string, args interface{}, reply interface{}) error { +// panic("mock out the Call method") +// }, +// } +// +// // use mockedRPCClient in code that requires RPCClient +// // and then make assertions. +// +// } +type RPCClientMock struct { + // CallFunc mocks the Call method. + CallFunc func(serviceMethod string, args interface{}, reply interface{}) error + + // calls tracks calls to the methods. + calls struct { + // Call holds details about calls to the Call method. + Call []struct { + // ServiceMethod is the serviceMethod argument value. + ServiceMethod string + // Args is the args argument value. + Args interface{} + // Reply is the reply argument value. + Reply interface{} + } + } + lockCall sync.RWMutex +} + +// Call calls CallFunc. +func (mock *RPCClientMock) Call(serviceMethod string, args interface{}, reply interface{}) error { + if mock.CallFunc == nil { + panic("RPCClientMock.CallFunc: method is nil but RPCClient.Call was just called") + } + callInfo := struct { + ServiceMethod string + Args interface{} + Reply interface{} + }{ + ServiceMethod: serviceMethod, + Args: args, + Reply: reply, + } + mock.lockCall.Lock() + mock.calls.Call = append(mock.calls.Call, callInfo) + mock.lockCall.Unlock() + return mock.CallFunc(serviceMethod, args, reply) +} + +// CallCalls gets all the calls that were made to Call. +// Check the length with: +// len(mockedRPCClient.CallCalls()) +func (mock *RPCClientMock) CallCalls() []struct { + ServiceMethod string + Args interface{} + Reply interface{} +} { + var calls []struct { + ServiceMethod string + Args interface{} + Reply interface{} + } + mock.lockCall.RLock() + calls = mock.calls.Call + mock.lockCall.RUnlock() + return calls +} diff --git a/app/plugin/conductor.go b/app/plugin/conductor.go new file mode 100644 index 0000000..657d20d --- /dev/null +++ b/app/plugin/conductor.go @@ -0,0 +1,232 @@ +// Package plugin provides support for RPC plugins with registration server. +// It also implements middleware calling all the registered and alive plugins +package plugin + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "sync" + "time" + + log "github.com/go-pkgz/lgr" + + "github.com/umputun/reproxy/app/discovery" + "github.com/umputun/reproxy/lib" +) + +//go:generate moq -out dialer_mock.go -fmt goimports . RPCDialer +//go:generate moq -out client_mock.go -fmt goimports . RPCClient + +// Conductor accepts registrations from rpc plugins, keeps list of active/current plugins and provides middleware calling all of them. +type Conductor struct { + Address string + RPCDialer RPCDialer + + plugins []Handler + lock sync.RWMutex +} + +// Handler contains information about a plugin's handler +type Handler struct { + Address string + Method string // full method name for rpc call, i.e. Plugin.Thing + Alive bool + client RPCClient +} + +// conductorCtxtKey used to retrieve conductor from context +type conductorCtxtKey string + +// CtxMatch key used to retrieve matching request info from the request context +const CtxMatch = conductorCtxtKey("match") + +// RPCDialer is a maker interface dialing to rpc server and returning new RPCClient +type RPCDialer interface { + Dial(network, address string) (RPCClient, error) +} + +// RPCDialerFunc is an adapter to allow the use of an ordinary functions as the RPCDialer. +type RPCDialerFunc func(network, address string) (RPCClient, error) + +// Dial rpc server +func (f RPCDialerFunc) Dial(network, address string) (RPCClient, error) { + return f(network, address) +} + +// RPCClient defines interface for remote calls +type RPCClient interface { + Call(serviceMethod string, args interface{}, reply interface{}) error +} + +// Run creates and activates http registration server +// TODO: add some basic auth in case if exposed by accident +func (c *Conductor) Run(ctx context.Context) error { + log.Printf("[INFO] start plugin conductor on %s", c.Address) + httpServer := &http.Server{ + Addr: c.Address, + Handler: c.registrationHandler(), + ReadHeaderTimeout: 50 * time.Millisecond, + WriteTimeout: 50 * time.Millisecond, + IdleTimeout: 50 * time.Millisecond, + } + + go func() { + <-ctx.Done() + if err := httpServer.Close(); err != nil { + log.Printf("[ERROR] failed to close plugin registration server, %v", err) + } + }() + + return httpServer.ListenAndServe() +} + +// Middleware hits all registered, alive-only plugins and modifies the original request accordingly +// Failed plugin calls ignored. Status code from any plugin may stop the chain of calls if not 200. This is needed +// to allow plugins like auth which has to terminate request in some cases. +func (c *Conductor) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + c.lock.RLock() + for _, p := range c.plugins { + if !p.Alive { + continue + } + + var reply lib.Response + if err := p.client.Call(p.Method, c.makeRequest(r), &reply); err != nil { + log.Printf("[WARN] failed to invoke plugin handler %s: %v", p.Method, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + for k, vv := range reply.HeadersIn { + for _, v := range vv { + r.Header.Add(k, v) + } + } + for k, vv := range reply.HeadersOut { + for _, v := range vv { + w.Header().Add(k, v) + } + } + if reply.StatusCode >= 400 { + c.lock.RUnlock() + http.Error(w, http.StatusText(reply.StatusCode), reply.StatusCode) + return + } + } + c.lock.RUnlock() + + next.ServeHTTP(w, r) + }) +} + +// makeRequest creates plugin request from http.Request +// uses context set by downstream (by proxyHandler) +func (c *Conductor) makeRequest(r *http.Request) lib.Request { + ctx := r.Context() + res := lib.Request{ + URL: r.URL.String(), + RemoteAddr: r.RemoteAddr, + Host: r.URL.Hostname(), + Header: r.Header, + } + + if v, ok := ctx.Value(CtxMatch).(discovery.MatchedRoute); ok { + res.Route = v.Destination + res.Match.MatchType = v.Mapper.MatchType.String() + res.Match.ProviderID = string(v.Mapper.ProviderID) + res.Match.Server = v.Mapper.Server + res.Match.Src = v.Mapper.SrcMatch.String() + res.Match.Dst = v.Mapper.Dst + res.Match.PingURL = v.Mapper.PingURL + res.Match.AssetsLocation = v.Mapper.AssetsLocation + res.Match.AssetsWebRoot = v.Mapper.AssetsWebRoot + } + + return res +} + +// registrationHandler accept POST or DELETE with lib.Plugin body and register/unregister plugin provider +func (c *Conductor) registrationHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "POST": + var plugin lib.Plugin + if err := json.NewDecoder(r.Body).Decode(&plugin); err != nil { + http.Error(w, "plugin registration failed", http.StatusBadRequest) + return + } + c.locked(func() { + if err := c.register(plugin); err != nil { + log.Printf("[WARN] rpc registration failed, %v", err) + http.Error(w, "rpc registration failed", http.StatusInternalServerError) + return + } + }) + case "DELETE": + var plugin lib.Plugin + if err := json.NewDecoder(r.Body).Decode(&plugin); err != nil { + http.Error(w, "failed to unregister plugin", http.StatusBadRequest) + return + } + c.locked(func() { c.unregister(plugin) }) + default: + http.Error(w, "invalid request type", http.StatusBadRequest) + } + }) +} + +// register plugin, not thread safe! call should be enclosed with lock +// creates tcp client, retrieves list of handlers (methods) and adds each one with the full method name +func (c *Conductor) register(p lib.Plugin) error { + + // collect all handlers after registration + var pp []Handler //nolint + for _, h := range c.plugins { + if strings.HasPrefix(h.Method, p.Name+".") && h.Address == p.Address { // already registered + log.Printf("[WARN] plugin %+v already registered", p) + return nil + } + + if strings.HasPrefix(h.Method, p.Name+".") && h.Address != p.Address { // registered, but address changed + log.Printf("[WARN] plugin %+v already registered, but address changed to %s", h, p.Address) + continue // remove from the collected pp + } + pp = append(pp, h) + } + + client, err := c.RPCDialer.Dial("tcp", p.Address) + if err != nil { + return fmt.Errorf("can't reach plugin %+v: %v", p, err) + } + + for _, l := range p.Methods { + handler := Handler{client: client, Alive: true, Address: p.Address, Method: p.Name + "." + l} + pp = append(pp, handler) + log.Printf("[INFO] register plugin %s, ip: %s, method: %s", p.Name, p.Address, handler.Method) + } + c.plugins = pp + return nil +} + +// unregister plugin, not thread safe! call should be enclosed with lock +func (c *Conductor) unregister(p lib.Plugin) { + log.Printf("[INFO] unregister plugin %s, ip: %s", p.Name, p.Address) + var res []Handler //nolint + for _, h := range c.plugins { + if strings.HasPrefix(h.Method, p.Name+".") { + continue + } + res = append(res, h) + } + c.plugins = res +} + +func (c *Conductor) locked(fn func()) { + c.lock.Lock() + fn() + c.lock.Unlock() +} diff --git a/app/plugin/conductor_test.go b/app/plugin/conductor_test.go new file mode 100644 index 0000000..99a1191 --- /dev/null +++ b/app/plugin/conductor_test.go @@ -0,0 +1,422 @@ +package plugin + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "math/rand" + "net/http" + "net/http/httptest" + "regexp" + "strconv" + "testing" + "time" + + log "github.com/go-pkgz/lgr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/umputun/reproxy/app/discovery" + "github.com/umputun/reproxy/lib" +) + +func TestConductor_registrationHandler(t *testing.T) { + + rpcClient := &RPCClientMock{ + CallFunc: func(serviceMethod string, args interface{}, reply interface{}) error { + return nil + }, + } + + dialer := &RPCDialerMock{ + DialFunc: func(network string, address string) (RPCClient, error) { + return rpcClient, nil + }, + } + + c := Conductor{RPCDialer: dialer} + ts := httptest.NewServer(c.registrationHandler()) + defer ts.Close() + + client := http.Client{Timeout: time.Second} + + { // register plugin with two methods + plugin := lib.Plugin{Name: "Test1", Address: "127.0.0.1:0001", Methods: []string{"Mw1", "Mw2"}} + data, err := json.Marshal(plugin) + require.NoError(t, err) + req, err := http.NewRequest("POST", ts.URL, bytes.NewReader(data)) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + assert.Equal(t, 2, len(c.plugins), "two plugins registered") + assert.Equal(t, "Test1.Mw1", c.plugins[0].Method) + assert.Equal(t, "127.0.0.1:0001", c.plugins[0].Address) + assert.Equal(t, true, c.plugins[0].Alive) + + assert.Equal(t, "127.0.0.1:0001", c.plugins[1].Address) + assert.Equal(t, "Test1.Mw2", c.plugins[1].Method) + assert.Equal(t, true, c.plugins[1].Alive) + + assert.Equal(t, 0, len(rpcClient.CallCalls())) + assert.Equal(t, 1, len(dialer.DialCalls())) + } + + { // same registration + plugin := lib.Plugin{Name: "Test1", Address: "127.0.0.1:0001", Methods: []string{"Mw1", "Mw2"}} + data, err := json.Marshal(plugin) + require.NoError(t, err) + req, err := http.NewRequest("POST", ts.URL, bytes.NewReader(data)) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, 2, len(c.plugins), "two plugins registered") + assert.Equal(t, 0, len(rpcClient.CallCalls())) + assert.Equal(t, 1, len(dialer.DialCalls())) + } + + { // address changed + plugin := lib.Plugin{Name: "Test1", Address: "127.0.0.2:8002", Methods: []string{"Mw1", "Mw2"}} + data, err := json.Marshal(plugin) + require.NoError(t, err) + req, err := http.NewRequest("POST", ts.URL, bytes.NewReader(data)) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, 2, len(c.plugins), "two plugins registered") + assert.Equal(t, "Test1.Mw1", c.plugins[0].Method) + assert.Equal(t, "127.0.0.2:8002", c.plugins[0].Address) + assert.Equal(t, true, c.plugins[0].Alive) + + assert.Equal(t, "127.0.0.2:8002", c.plugins[1].Address) + assert.Equal(t, "Test1.Mw2", c.plugins[1].Method) + assert.Equal(t, true, c.plugins[1].Alive) + + assert.Equal(t, 0, len(rpcClient.CallCalls())) + assert.Equal(t, 2, len(dialer.DialCalls())) + } + + { // address changed + plugin := lib.Plugin{Name: "Test2", Address: "127.0.0.3:8003", Methods: []string{"Mw11", "Mw12", "Mw13"}} + data, err := json.Marshal(plugin) + require.NoError(t, err) + req, err := http.NewRequest("POST", ts.URL, bytes.NewReader(data)) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, 2+3, len(c.plugins), "3 more plugins registered") + assert.Equal(t, "Test2.Mw11", c.plugins[2].Method) + assert.Equal(t, "127.0.0.3:8003", c.plugins[2].Address) + assert.Equal(t, true, c.plugins[2].Alive) + + assert.Equal(t, 0, len(rpcClient.CallCalls())) + assert.Equal(t, 3, len(dialer.DialCalls())) + } + + { // bad registration + req, err := http.NewRequest("POST", ts.URL, bytes.NewBufferString("bas json body")) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + } + + { // unsupported registration method + plugin := lib.Plugin{Name: "Test2", Address: "127.0.0.3:8003", Methods: []string{"Mw11", "Mw12", "Mw13"}} + data, err := json.Marshal(plugin) + require.NoError(t, err) + req, err := http.NewRequest("PUT", ts.URL, bytes.NewReader(data)) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + } + + { // unregister + plugin := lib.Plugin{Name: "Test1", Address: "127.0.0.2:8002", Methods: []string{"Mw1", "Mw2"}} + data, err := json.Marshal(plugin) + require.NoError(t, err) + req, err := http.NewRequest("DELETE", ts.URL, bytes.NewReader(data)) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, 3, len(c.plugins), "3 plugins left, 2 removed") + + assert.Equal(t, "Test2.Mw11", c.plugins[0].Method) + assert.Equal(t, "127.0.0.3:8003", c.plugins[0].Address) + assert.Equal(t, true, c.plugins[0].Alive) + + assert.Equal(t, 0, len(rpcClient.CallCalls())) + assert.Equal(t, 3, len(dialer.DialCalls())) + } + + { // bad unregister + req, err := http.NewRequest("DELETE", ts.URL, bytes.NewBufferString("bad json body")) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + assert.Equal(t, 3, len(c.plugins), "still 3 plugins left, 2 removed") + } +} + +func TestConductor_registrationHandlerInternalError(t *testing.T) { + + dialer := &RPCDialerMock{ + DialFunc: func(network string, address string) (RPCClient, error) { + return nil, errors.New("failed") + }, + } + + c := Conductor{RPCDialer: dialer} + ts := httptest.NewServer(c.registrationHandler()) + defer ts.Close() + + client := http.Client{Timeout: time.Second} + plugin := lib.Plugin{Name: "Test1", Address: "127.0.0.1:0001"} + data, err := json.Marshal(plugin) + require.NoError(t, err) + req, err := http.NewRequest("POST", ts.URL, bytes.NewReader(data)) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) +} + +func TestConductor_Middleware(t *testing.T) { + + rpcClient := &RPCClientMock{ + CallFunc: func(serviceMethod string, args interface{}, reply interface{}) error { + + if serviceMethod == "Test1.Mw1" { + req := args.(lib.Request) + assert.Equal(t, "route123", req.Route) + assert.Equal(t, "src123", req.Match.Src) + assert.Equal(t, "dst123", req.Match.Dst) + assert.Equal(t, "docker", req.Match.ProviderID) + assert.Equal(t, "server123", req.Match.Server) + assert.Equal(t, "proxy", req.Match.MatchType) + assert.Equal(t, "/webroot", req.Match.AssetsWebRoot) + assert.Equal(t, "loc", req.Match.AssetsLocation) + log.Printf("rr: %+v", req) + reply.(*lib.Response).StatusCode = 200 + reply.(*lib.Response).HeadersOut = map[string][]string{} + reply.(*lib.Response).HeadersOut.Set("k1", "v1") + reply.(*lib.Response).HeadersIn = map[string][]string{} + reply.(*lib.Response).HeadersIn.Set("k21", "v21") + + } + if serviceMethod == "Test1.Mw2" { + req := args.(lib.Request) + assert.Equal(t, "route123", req.Route) + assert.Equal(t, "src123", req.Match.Src) + assert.Equal(t, "dst123", req.Match.Dst) + assert.Equal(t, "docker", req.Match.ProviderID) + assert.Equal(t, "server123", req.Match.Server) + log.Printf("rr: %+v", req) + reply.(*lib.Response).StatusCode = 200 + reply.(*lib.Response).HeadersOut = map[string][]string{} + reply.(*lib.Response).HeadersOut.Set("k11", "v11") + } + if serviceMethod == "Test1.Mw3" { + t.Fatal("shouldn't be called") + } + return nil + }, + } + + dialer := &RPCDialerMock{ + DialFunc: func(network string, address string) (RPCClient, error) { + return rpcClient, nil + }, + } + + c := Conductor{RPCDialer: dialer, Address: "127.0.0.1:50100"} + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + go func() { + c.Run(ctx) + }() + time.Sleep(time.Millisecond * 50) + + client := http.Client{Timeout: time.Second} + + // register plugin with 3 methods + plugin := lib.Plugin{Name: "Test1", Address: "127.0.0.1:8001", Methods: []string{"Mw1", "Mw2", "Mw3"}} + data, err := json.Marshal(plugin) + require.NoError(t, err) + req, err := http.NewRequest("POST", "http://127.0.0.1:50100", bytes.NewReader(data)) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, 3, len(c.plugins), "3 plugins registered") + c.plugins[2].Alive = false // set 3rd to dead + + rr, err := http.NewRequest("GET", "http://127.0.0.1", nil) + require.NoError(t, err) + + m := discovery.MatchedRoute{ + Destination: "route123", + Mapper: discovery.URLMapper{ + Server: "server123", + ProviderID: discovery.PIDocker, + MatchType: discovery.MTProxy, + SrcMatch: *regexp.MustCompile("src123"), + Dst: "dst123", + AssetsWebRoot: "/webroot", + AssetsLocation: "loc", + }, + } + rr = rr.WithContext(context.WithValue(rr.Context(), CtxMatch, m)) + w := httptest.NewRecorder() + h := c.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("k2", "v2") + w.Write([]byte("something")) + assert.Equal(t, "v21", r.Header.Get("k21")) + })) + h.ServeHTTP(w, rr) + assert.Equal(t, 200, w.Result().StatusCode) + assert.Equal(t, "v1", w.Result().Header.Get("k1")) + assert.Equal(t, "v2", w.Result().Header.Get("k2")) + assert.Equal(t, "v21", rr.Header.Get("k21")) + t.Logf("req: %+v", rr) + t.Logf("resp: %+v", w.Result()) +} + +func TestConductor_MiddlewarePluginBadStatus(t *testing.T) { + + rpcClient := &RPCClientMock{ + CallFunc: func(serviceMethod string, args interface{}, reply interface{}) error { + if serviceMethod == "Test1.Mw1" { + req := args.(lib.Request) + assert.Equal(t, "route123", req.Route) + assert.Equal(t, "src123", req.Match.Src) + assert.Equal(t, "dst123", req.Match.Dst) + assert.Equal(t, "docker", req.Match.ProviderID) + assert.Equal(t, "server123", req.Match.Server) + log.Printf("rr: %+v", req) + reply.(*lib.Response).StatusCode = 404 + } + return nil + }, + } + + dialer := &RPCDialerMock{ + DialFunc: func(network string, address string) (RPCClient, error) { + return rpcClient, nil + }, + } + + rand.Seed(time.Now().UnixNano()) + port := rand.Intn(30000) + c := Conductor{RPCDialer: dialer, Address: "127.0.0.1:" + strconv.Itoa(30000+port)} + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + go func() { + c.Run(ctx) + }() + time.Sleep(time.Millisecond * 150) + + client := http.Client{Timeout: time.Second} + + // register plugin with one methods + plugin := lib.Plugin{Name: "Test1", Address: "127.0.0.1:8001", Methods: []string{"Mw1"}} + data, err := json.Marshal(plugin) + require.NoError(t, err) + req, err := http.NewRequest("POST", "http://127.0.0.1:"+strconv.Itoa(30000+port), bytes.NewReader(data)) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, 1, len(c.plugins), "one plugin registered") + + rr, err := http.NewRequest("GET", "http://127.0.0.1", nil) + require.NoError(t, err) + + m := discovery.MatchedRoute{ + Destination: "route123", + Mapper: discovery.URLMapper{ + Server: "server123", + ProviderID: discovery.PIDocker, + MatchType: discovery.MTProxy, + SrcMatch: *regexp.MustCompile("src123"), + Dst: "dst123", + AssetsWebRoot: "/webroot", + AssetsLocation: "loc", + }, + } + rr = rr.WithContext(context.WithValue(rr.Context(), CtxMatch, m)) + w := httptest.NewRecorder() + h := c.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Failed() // handler not called on plugin middleware error + })) + h.ServeHTTP(w, rr) + assert.Equal(t, 404, w.Result().StatusCode) + assert.Equal(t, "", rr.Header.Get("k1")) // header not set by plugin on error + t.Logf("req: %+v", rr) + t.Logf("resp: %+v", w.Result()) +} + +func TestConductor_MiddlewarePluginFailed(t *testing.T) { + + rpcClient := &RPCClientMock{ + CallFunc: func(serviceMethod string, args interface{}, reply interface{}) error { + if serviceMethod == "Test1.Mw1" { + return errors.New("something failed") + } + return nil + }, + } + + dialer := &RPCDialerMock{ + DialFunc: func(network string, address string) (RPCClient, error) { + return rpcClient, nil + }, + } + + c := Conductor{RPCDialer: dialer, Address: "127.0.0.1:50100"} + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + go func() { + c.Run(ctx) + }() + time.Sleep(time.Millisecond * 250) + + client := http.Client{Timeout: time.Second} + + // register plugin with one methods + plugin := lib.Plugin{Name: "Test1", Address: "127.0.0.1:8001", Methods: []string{"Mw1"}} + data, err := json.Marshal(plugin) + require.NoError(t, err) + req, err := http.NewRequest("POST", "http://127.0.0.1:50100", bytes.NewReader(data)) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, 1, len(c.plugins), "one plugin registered") + + rr, err := http.NewRequest("GET", "http://127.0.0.1", nil) + require.NoError(t, err) + w := httptest.NewRecorder() + h := c.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Failed() // handler not called on plugin middleware error + })) + h.ServeHTTP(w, rr) + assert.Equal(t, 500, w.Result().StatusCode) + assert.Equal(t, "", rr.Header.Get("k1")) // header not set by plugin on error + t.Logf("req: %+v", rr) + t.Logf("resp: %+v", w.Result()) +} diff --git a/app/plugin/dialer_mock.go b/app/plugin/dialer_mock.go new file mode 100644 index 0000000..39e70a8 --- /dev/null +++ b/app/plugin/dialer_mock.go @@ -0,0 +1,79 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package plugin + +import ( + "sync" +) + +// Ensure, that RPCDialerMock does implement RPCDialer. +// If this is not the case, regenerate this file with moq. +var _ RPCDialer = &RPCDialerMock{} + +// RPCDialerMock is a mock implementation of RPCDialer. +// +// func TestSomethingThatUsesRPCDialer(t *testing.T) { +// +// // make and configure a mocked RPCDialer +// mockedRPCDialer := &RPCDialerMock{ +// DialFunc: func(network string, address string) (RPCClient, error) { +// panic("mock out the Dial method") +// }, +// } +// +// // use mockedRPCDialer in code that requires RPCDialer +// // and then make assertions. +// +// } +type RPCDialerMock struct { + // DialFunc mocks the Dial method. + DialFunc func(network string, address string) (RPCClient, error) + + // calls tracks calls to the methods. + calls struct { + // Dial holds details about calls to the Dial method. + Dial []struct { + // Network is the network argument value. + Network string + // Address is the address argument value. + Address string + } + } + lockDial sync.RWMutex +} + +// Dial calls DialFunc. +func (mock *RPCDialerMock) Dial(network string, address string) (RPCClient, error) { + if mock.DialFunc == nil { + panic("RPCDialerMock.DialFunc: method is nil but RPCDialer.Dial was just called") + } + callInfo := struct { + Network string + Address string + }{ + Network: network, + Address: address, + } + mock.lockDial.Lock() + mock.calls.Dial = append(mock.calls.Dial, callInfo) + mock.lockDial.Unlock() + return mock.DialFunc(network, address) +} + +// DialCalls gets all the calls that were made to Dial. +// Check the length with: +// len(mockedRPCDialer.DialCalls()) +func (mock *RPCDialerMock) DialCalls() []struct { + Network string + Address string +} { + var calls []struct { + Network string + Address string + } + mock.lockDial.RLock() + calls = mock.calls.Dial + mock.lockDial.RUnlock() + return calls +} diff --git a/app/proxy/handlers.go b/app/proxy/handlers.go new file mode 100644 index 0000000..453c378 --- /dev/null +++ b/app/proxy/handlers.go @@ -0,0 +1,117 @@ +package proxy + +import ( + "io" + "net/http" + "strings" + + log "github.com/go-pkgz/lgr" + R "github.com/go-pkgz/rest" + "github.com/gorilla/handlers" +) + +func headersHandler(headers []string) func(next http.Handler) http.Handler { + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if len(headers) == 0 { + next.ServeHTTP(w, r) + return + } + for _, h := range headers { + elems := strings.Split(h, ":") + if len(elems) != 2 { + continue + } + w.Header().Set(strings.TrimSpace(elems[0]), strings.TrimSpace(elems[1])) + } + next.ServeHTTP(w, r) + }) + } +} + +func maxReqSizeHandler(maxSize int64) func(next http.Handler) http.Handler { + if maxSize <= 0 { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + }) + } + } + + log.Printf("[DEBUG] request size limited to %d", maxSize) + return func(next http.Handler) http.Handler { + + fn := func(w http.ResponseWriter, r *http.Request) { + + // check ContentLength + if r.ContentLength > maxSize { + w.WriteHeader(http.StatusRequestEntityTooLarge) + return + } + + r.Body = http.MaxBytesReader(w, r.Body, maxSize) + if err := r.ParseForm(); err != nil { + http.Error(w, "Request Entity Too Large", http.StatusRequestEntityTooLarge) + return + } + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) + } +} + +func accessLogHandler(wr io.Writer) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return handlers.CombinedLoggingHandler(wr, next) + } +} + +func stdoutLogHandler(enable bool, lh func(next http.Handler) http.Handler) func(next http.Handler) http.Handler { + + if enable { + log.Printf("[DEBUG] stdout logging enabled") + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + // don't log to stdout GET ~/(.*)/ping$ requests + if r.Method == "GET" && strings.HasSuffix(r.URL.Path, "/ping") { + next.ServeHTTP(w, r) + return + } + lh(next).ServeHTTP(w, r) + } + return http.HandlerFunc(fn) + } + } + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + }) + } +} + +func gzipHandler(enabled bool) func(next http.Handler) http.Handler { + if enabled { + log.Printf("[DEBUG] gzip enabled") + return handlers.CompressHandler + } + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + }) + } +} + +func signatureHandler(enabled bool, version string) func(next http.Handler) http.Handler { + if enabled { + log.Printf("[DEBUG] signature headers enabled") + return R.AppInfo("reproxy", "umputun", version) + } + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + }) + } +} diff --git a/app/proxy/handlers_test.go b/app/proxy/handlers_test.go new file mode 100644 index 0000000..0ecf55d --- /dev/null +++ b/app/proxy/handlers_test.go @@ -0,0 +1,85 @@ +package proxy + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_headersHandler(t *testing.T) { + wr := httptest.NewRecorder() + handler := headersHandler([]string{"k1:v1", "k2:v2"})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("req: %v", r) + })) + req, err := http.NewRequest("GET", "http://example.com", nil) + require.NoError(t, err) + handler.ServeHTTP(wr, req) + assert.Equal(t, "v1", wr.Result().Header.Get("k1")) + assert.Equal(t, "v2", wr.Result().Header.Get("k2")) +} + +func Test_maxReqSizeHandler(t *testing.T) { + { + wr := httptest.NewRecorder() + handler := maxReqSizeHandler(10)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("req: %v", r) + })) + req, err := http.NewRequest("POST", "http://example.com", bytes.NewBufferString("123456")) + require.NoError(t, err) + handler.ServeHTTP(wr, req) + assert.Equal(t, http.StatusOK, wr.Result().StatusCode, "good size, full response") + } + { + wr := httptest.NewRecorder() + handler := maxReqSizeHandler(10)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("req: %v", r) + })) + req, err := http.NewRequest("POST", "http://example.com", bytes.NewBufferString("123456789012345")) + require.NoError(t, err) + handler.ServeHTTP(wr, req) + assert.Equal(t, http.StatusRequestEntityTooLarge, wr.Result().StatusCode) + } + { + wr := httptest.NewRecorder() + handler := maxReqSizeHandler(0)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("req: %v", r) + })) + req, err := http.NewRequest("POST", "http://example.com", bytes.NewBufferString("123456")) + require.NoError(t, err) + handler.ServeHTTP(wr, req) + assert.Equal(t, http.StatusOK, wr.Result().StatusCode, "good size, full response") + } +} + +func Test_signatureHandler(t *testing.T) { + { + wr := httptest.NewRecorder() + handler := signatureHandler(true, "v0.0.1")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("req: %v", r) + })) + req, err := http.NewRequest("POST", "http://example.com", bytes.NewBufferString("123456")) + require.NoError(t, err) + handler.ServeHTTP(wr, req) + assert.Equal(t, http.StatusOK, wr.Result().StatusCode) + assert.Equal(t, "reproxy", wr.Result().Header.Get("App-Name"), wr.Result().Header) + assert.Equal(t, "umputun", wr.Result().Header.Get("Author"), wr.Result().Header) + assert.Equal(t, "v0.0.1", wr.Result().Header.Get("App-Version"), wr.Result().Header) + } + { + wr := httptest.NewRecorder() + handler := signatureHandler(false, "v0.0.1")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("req: %v", r) + })) + req, err := http.NewRequest("POST", "http://example.com", bytes.NewBufferString("123456")) + require.NoError(t, err) + handler.ServeHTTP(wr, req) + assert.Equal(t, http.StatusOK, wr.Result().StatusCode) + assert.Equal(t, "", wr.Result().Header.Get("App-Name"), wr.Result().Header) + assert.Equal(t, "", wr.Result().Header.Get("Author"), wr.Result().Header) + assert.Equal(t, "", wr.Result().Header.Get("App-Version"), wr.Result().Header) + } +} diff --git a/app/proxy/matcher_mock.go b/app/proxy/matcher_mock.go new file mode 100644 index 0000000..a808ef6 --- /dev/null +++ b/app/proxy/matcher_mock.go @@ -0,0 +1,189 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package proxy + +import ( + "sync" + + "github.com/umputun/reproxy/app/discovery" +) + +// Ensure, that MatcherMock does implement Matcher. +// If this is not the case, regenerate this file with moq. +var _ Matcher = &MatcherMock{} + +// MatcherMock is a mock implementation of Matcher. +// +// func TestSomethingThatUsesMatcher(t *testing.T) { +// +// // make and configure a mocked Matcher +// mockedMatcher := &MatcherMock{ +// CheckHealthFunc: func() map[string]error { +// panic("mock out the CheckHealth method") +// }, +// MappersFunc: func() []discovery.URLMapper { +// panic("mock out the Mappers method") +// }, +// MatchFunc: func(srv string, src string) discovery.Matches { +// panic("mock out the Match method") +// }, +// ServersFunc: func() []string { +// panic("mock out the Servers method") +// }, +// } +// +// // use mockedMatcher in code that requires Matcher +// // and then make assertions. +// +// } +type MatcherMock struct { + // CheckHealthFunc mocks the CheckHealth method. + CheckHealthFunc func() map[string]error + + // MappersFunc mocks the Mappers method. + MappersFunc func() []discovery.URLMapper + + // MatchFunc mocks the Match method. + MatchFunc func(srv string, src string) discovery.Matches + + // ServersFunc mocks the Servers method. + ServersFunc func() []string + + // calls tracks calls to the methods. + calls struct { + // CheckHealth holds details about calls to the CheckHealth method. + CheckHealth []struct { + } + // Mappers holds details about calls to the Mappers method. + Mappers []struct { + } + // Match holds details about calls to the Match method. + Match []struct { + // Srv is the srv argument value. + Srv string + // Src is the src argument value. + Src string + } + // Servers holds details about calls to the Servers method. + Servers []struct { + } + } + lockCheckHealth sync.RWMutex + lockMappers sync.RWMutex + lockMatch sync.RWMutex + lockServers sync.RWMutex +} + +// CheckHealth calls CheckHealthFunc. +func (mock *MatcherMock) CheckHealth() map[string]error { + if mock.CheckHealthFunc == nil { + panic("MatcherMock.CheckHealthFunc: method is nil but Matcher.CheckHealth was just called") + } + callInfo := struct { + }{} + mock.lockCheckHealth.Lock() + mock.calls.CheckHealth = append(mock.calls.CheckHealth, callInfo) + mock.lockCheckHealth.Unlock() + return mock.CheckHealthFunc() +} + +// CheckHealthCalls gets all the calls that were made to CheckHealth. +// Check the length with: +// len(mockedMatcher.CheckHealthCalls()) +func (mock *MatcherMock) CheckHealthCalls() []struct { +} { + var calls []struct { + } + mock.lockCheckHealth.RLock() + calls = mock.calls.CheckHealth + mock.lockCheckHealth.RUnlock() + return calls +} + +// Mappers calls MappersFunc. +func (mock *MatcherMock) Mappers() []discovery.URLMapper { + if mock.MappersFunc == nil { + panic("MatcherMock.MappersFunc: method is nil but Matcher.Mappers was just called") + } + callInfo := struct { + }{} + mock.lockMappers.Lock() + mock.calls.Mappers = append(mock.calls.Mappers, callInfo) + mock.lockMappers.Unlock() + return mock.MappersFunc() +} + +// MappersCalls gets all the calls that were made to Mappers. +// Check the length with: +// len(mockedMatcher.MappersCalls()) +func (mock *MatcherMock) MappersCalls() []struct { +} { + var calls []struct { + } + mock.lockMappers.RLock() + calls = mock.calls.Mappers + mock.lockMappers.RUnlock() + return calls +} + +// Match calls MatchFunc. +func (mock *MatcherMock) Match(srv string, src string) discovery.Matches { + if mock.MatchFunc == nil { + panic("MatcherMock.MatchFunc: method is nil but Matcher.Match was just called") + } + callInfo := struct { + Srv string + Src string + }{ + Srv: srv, + Src: src, + } + mock.lockMatch.Lock() + mock.calls.Match = append(mock.calls.Match, callInfo) + mock.lockMatch.Unlock() + return mock.MatchFunc(srv, src) +} + +// MatchCalls gets all the calls that were made to Match. +// Check the length with: +// len(mockedMatcher.MatchCalls()) +func (mock *MatcherMock) MatchCalls() []struct { + Srv string + Src string +} { + var calls []struct { + Srv string + Src string + } + mock.lockMatch.RLock() + calls = mock.calls.Match + mock.lockMatch.RUnlock() + return calls +} + +// Servers calls ServersFunc. +func (mock *MatcherMock) Servers() []string { + if mock.ServersFunc == nil { + panic("MatcherMock.ServersFunc: method is nil but Matcher.Servers was just called") + } + callInfo := struct { + }{} + mock.lockServers.Lock() + mock.calls.Servers = append(mock.calls.Servers, callInfo) + mock.lockServers.Unlock() + return mock.ServersFunc() +} + +// ServersCalls gets all the calls that were made to Servers. +// Check the length with: +// len(mockedMatcher.ServersCalls()) +func (mock *MatcherMock) ServersCalls() []struct { +} { + var calls []struct { + } + mock.lockServers.RLock() + calls = mock.calls.Servers + mock.lockServers.RUnlock() + return calls +} diff --git a/app/proxy/proxy.go b/app/proxy/proxy.go index 5300155..ea9b2f5 100644 --- a/app/proxy/proxy.go +++ b/app/proxy/proxy.go @@ -17,31 +17,31 @@ import ( log "github.com/go-pkgz/lgr" R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/logger" - "github.com/gorilla/handlers" "github.com/umputun/reproxy/app/discovery" - "github.com/umputun/reproxy/app/mgmt" + "github.com/umputun/reproxy/app/plugin" ) // Http is a proxy server for both http and https type Http struct { // nolint golint Matcher - Address string - AssetsLocation string - AssetsWebRoot string - MaxBodySize int64 - GzEnabled bool - ProxyHeaders []string - SSLConfig SSLConfig - Version string - AccessLog io.Writer - StdOutEnabled bool - Signature bool - Timeouts Timeouts - CacheControl MiddlewareProvider - Metrics MiddlewareProvider - Reporter Reporter - LBSelector func(len int) int + Address string + AssetsLocation string + AssetsWebRoot string + MaxBodySize int64 + GzEnabled bool + ProxyHeaders []string + SSLConfig SSLConfig + Version string + AccessLog io.Writer + StdOutEnabled bool + Signature bool + Timeouts Timeouts + CacheControl MiddlewareProvider + Metrics MiddlewareProvider + PluginConductor MiddlewareProvider + Reporter Reporter + LBSelector func(len int) int } // Matcher source info (server and route) to the destination url @@ -107,15 +107,17 @@ func (h *Http) Run(ctx context.Context) error { handler := R.Wrap(h.proxyHandler(), R.Recoverer(log.Default()), - h.signatureHandler(), + signatureHandler(h.Signature, h.Version), h.pingHandler, h.healthMiddleware, + h.matchHandler, h.mgmtHandler(), - h.headersHandler(h.ProxyHeaders), - h.accessLogHandler(h.AccessLog), - h.stdoutLogHandler(h.StdOutEnabled, logger.New(logger.Log(log.Default()), logger.Prefix("[INFO]")).Handler), - h.maxReqSizeHandler(h.MaxBodySize), - h.gzipHandler(), + h.pluginHandler(), + headersHandler(h.ProxyHeaders), + accessLogHandler(h.AccessLog), + stdoutLogHandler(h.StdOutEnabled, logger.New(logger.Log(log.Default()), logger.Prefix("[INFO]")).Handler), + maxReqSizeHandler(h.MaxBodySize), + gzipHandler(h.GzEnabled), ) if len(h.SSLConfig.FQDNs) == 0 && h.SSLConfig.SSLMode == SSLAuto { @@ -172,8 +174,14 @@ func (h *Http) Run(ctx context.Context) error { return fmt.Errorf("unknown SSL type %v", h.SSLConfig.SSLMode) } +type contextKey string + +const ( + ctxURL = contextKey("url") + ctxMatchType = contextKey("type") +) + func (h *Http) proxyHandler() http.HandlerFunc { - type contextKey string reverseProxy := &httputil.ReverseProxy{ Director: func(r *http.Request) { @@ -204,13 +212,8 @@ func (h *Http) proxyHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - server := r.URL.Hostname() - if server == "" { - server = strings.Split(r.Host, ":")[0] - } - matches := h.Match(server, r.URL.Path) // get all matches for the server:path pair - u, ok := h.getMatch(matches) // pick a single match from alive only, uses LBSelector as the strategy - if !ok { // no route match + uuVal := r.Context().Value(ctxURL) + if uuVal == nil { // no route match detected by matchHandler if h.isAssetRequest(r) { assetsHandler.ServeHTTP(w, r) return @@ -219,20 +222,18 @@ func (h *Http) proxyHandler() http.HandlerFunc { h.Reporter.Report(w, http.StatusBadGateway) return } + uu := uuVal.(*url.URL) - switch matches.MatchType { + match := r.Context().Value(plugin.CtxMatch).(discovery.MatchedRoute) + matchType := r.Context().Value(ctxMatchType).(discovery.MatchType) + + switch matchType { case discovery.MTProxy: - uu, err := url.Parse(u) - if err != nil { - h.Reporter.Report(w, http.StatusBadGateway) - return - } log.Printf("[DEBUG] proxy to %s", uu) - ctx := context.WithValue(r.Context(), contextKey("url"), uu) // set destination url in request's context - reverseProxy.ServeHTTP(w, r.WithContext(ctx)) + reverseProxy.ServeHTTP(w, r) case discovery.MTStatic: // static match result has webroot:location, i.e. /www:/var/somedir/ - ae := strings.Split(u, ":") + ae := strings.Split(match.Destination, ":") if len(ae) != 2 { // shouldn't happen h.Reporter.Report(w, http.StatusInternalServerError) return @@ -247,26 +248,51 @@ func (h *Http) proxyHandler() http.HandlerFunc { } } -func (h *Http) getMatch(mm discovery.Matches) (u string, ok bool) { - if len(mm.Routes) == 0 { - return "", false - } +// matchHandler is a part of middleware chain. Matches incoming request to one or more matched rules +// and if match found sets it to the request context. Context used by proxy handler as well as by plugin conductor +func (h *Http) matchHandler(next http.Handler) http.Handler { - var urls []string // alive destinations only - for _, m := range mm.Routes { - if m.Alive { - urls = append(urls, m.Destination) + getMatch := func(mm discovery.Matches, picker func(len int) int) (m discovery.MatchedRoute, ok bool) { + if len(mm.Routes) == 0 { + return m, false + } + + var matches []discovery.MatchedRoute + for _, m := range mm.Routes { + if m.Alive { + matches = append(matches, m) + } + } + switch len(matches) { + case 0: + return m, false + case 1: + return matches[0], true + default: + return matches[picker(len(matches))], true } } - switch len(urls) { - case 0: - return "", false - case 1: - return urls[0], true - default: - return urls[h.LBSelector(len(urls))], true - } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := r.URL.Hostname() + if server == "" { + server = strings.Split(r.Host, ":")[0] + } + matches := h.Match(server, r.URL.Path) // get all matches for the server:path pair + match, ok := getMatch(matches, h.LBSelector) + if ok { + uu, err := url.Parse(match.Destination) + if err != nil { + h.Reporter.Report(w, http.StatusBadGateway) + return + } + ctx := context.WithValue(r.Context(), ctxURL, uu) // set destination url in request's context + ctx = context.WithValue(ctx, ctxMatchType, matches.MatchType) // set match type + ctx = context.WithValue(ctx, plugin.CtxMatch, match) // set keys for plugin conductor + r = r.WithContext(ctx) + } + next.ServeHTTP(w, r) + }) } func (h *Http) assetsHandler() http.HandlerFunc { @@ -295,23 +321,10 @@ func (h *Http) toHTTP(address string, httpPort int) string { return rx.ReplaceAllString(address, "$1:") + strconv.Itoa(httpPort) } -func (h *Http) gzipHandler() func(next http.Handler) http.Handler { - if h.GzEnabled { - log.Printf("[DEBUG] gzip enabled") - return handlers.CompressHandler - } - - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - next.ServeHTTP(w, r) - }) - } -} - -func (h *Http) signatureHandler() func(next http.Handler) http.Handler { - if h.Signature { - log.Printf("[DEBUG] signature headers enabled") - return R.AppInfo("reproxy", "umputun", h.Version) +func (h *Http) pluginHandler() func(next http.Handler) http.Handler { + if h.PluginConductor != nil { + log.Printf("[INFO] plugin support enabled") + return h.PluginConductor.Middleware } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -320,89 +333,8 @@ func (h *Http) signatureHandler() func(next http.Handler) http.Handler { } } -func (h *Http) headersHandler(headers []string) func(next http.Handler) http.Handler { - - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if len(h.ProxyHeaders) == 0 { - next.ServeHTTP(w, r) - return - } - for _, h := range headers { - elems := strings.Split(h, ":") - if len(elems) != 2 { - continue - } - w.Header().Set(strings.TrimSpace(elems[0]), strings.TrimSpace(elems[1])) - } - next.ServeHTTP(w, r) - }) - } -} - -func (h *Http) accessLogHandler(wr io.Writer) func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return handlers.CombinedLoggingHandler(wr, next) - } -} - -func (h *Http) stdoutLogHandler(enable bool, lh func(next http.Handler) http.Handler) func(next http.Handler) http.Handler { - - if enable { - log.Printf("[DEBUG] stdout logging enabled") - return func(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - // don't log to stdout GET ~/(.*)/ping$ requests - if r.Method == "GET" && strings.HasSuffix(r.URL.Path, "/ping") { - next.ServeHTTP(w, r) - return - } - lh(next).ServeHTTP(w, r) - } - return http.HandlerFunc(fn) - } - } - - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - next.ServeHTTP(w, r) - }) - } -} - -func (h *Http) maxReqSizeHandler(maxSize int64) func(next http.Handler) http.Handler { - if maxSize <= 0 { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - next.ServeHTTP(w, r) - }) - } - } - - log.Printf("[DEBUG] request size limited to %d", maxSize) - return func(next http.Handler) http.Handler { - - fn := func(w http.ResponseWriter, r *http.Request) { - - // check ContentLength - if r.ContentLength > maxSize { - w.WriteHeader(http.StatusRequestEntityTooLarge) - return - } - - r.Body = http.MaxBytesReader(w, r.Body, maxSize) - if err := r.ParseForm(); err != nil { - http.Error(w, "Request Entity Too Large", http.StatusRequestEntityTooLarge) - return - } - next.ServeHTTP(w, r) - } - return http.HandlerFunc(fn) - } -} - func (h *Http) mgmtHandler() func(next http.Handler) http.Handler { - if h.Metrics.(*mgmt.Metrics) != nil { // type assertion needed because we compare interface to nil + if h.Metrics != nil { log.Printf("[DEBUG] metrics enabled") return h.Metrics.Middleware } diff --git a/app/proxy/proxy_test.go b/app/proxy/proxy_test.go index cfe5da3..8750b4b 100644 --- a/app/proxy/proxy_test.go +++ b/app/proxy/proxy_test.go @@ -9,7 +9,9 @@ import ( "math/rand" "net/http" "net/http/httptest" + "net/url" "strconv" + "sync/atomic" "testing" "time" @@ -150,7 +152,7 @@ func TestHttp_DoWithAssets(t *testing.T) { body, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, "response /567/something", string(body)) - assert.Equal(t, "", resp.Header.Get("App-Name")) + assert.Equal(t, "", resp.Header.Get("App-Method")) assert.Equal(t, "v1", resp.Header.Get("h1")) } @@ -164,7 +166,7 @@ func TestHttp_DoWithAssets(t *testing.T) { body, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, "test html", string(body)) - assert.Equal(t, "", resp.Header.Get("App-Name")) + assert.Equal(t, "", resp.Header.Get("App-Method")) assert.Equal(t, "", resp.Header.Get("h1")) assert.Equal(t, "public, max-age=43200", resp.Header.Get("Cache-Control")) } @@ -231,7 +233,7 @@ func TestHttp_DoWithAssetRules(t *testing.T) { body, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, "response /567/something", string(body)) - assert.Equal(t, "", resp.Header.Get("App-Name")) + assert.Equal(t, "", resp.Header.Get("App-Method")) assert.Equal(t, "v1", resp.Header.Get("h1")) } @@ -245,7 +247,7 @@ func TestHttp_DoWithAssetRules(t *testing.T) { body, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, "test html", string(body)) - assert.Equal(t, "", resp.Header.Get("App-Name")) + assert.Equal(t, "", resp.Header.Get("App-Method")) assert.Equal(t, "", resp.Header.Get("h1")) assert.Equal(t, "public, max-age=43200", resp.Header.Get("Cache-Control")) } @@ -364,16 +366,23 @@ func TestHttp_isAssetRequest(t *testing.T) { } -func TestHttp_getMatch(t *testing.T) { +func TestHttp_matchHandler(t *testing.T) { tbl := []struct { matches discovery.Matches res string ok bool }{ + { - discovery.Matches{MatchType: discovery.MTProxy, Routes: []discovery.MatchedRoute{}}, "", false, + discovery.Matches{MatchType: discovery.MTProxy, Routes: []discovery.MatchedRoute{ + {Destination: "dest1", Alive: true}, + {Destination: "dest2", Alive: true}, + {Destination: "dest3", Alive: true}, + }}, + "dest1", true, }, + { discovery.Matches{MatchType: discovery.MTProxy, Routes: []discovery.MatchedRoute{ {Destination: "dest1", Alive: false}, @@ -392,11 +401,11 @@ func TestHttp_getMatch(t *testing.T) { }, { discovery.Matches{MatchType: discovery.MTProxy, Routes: []discovery.MatchedRoute{ - {Destination: "dest1", Alive: true}, - {Destination: "dest2", Alive: true}, + {Destination: "dest1", Alive: false}, + {Destination: "dest2", Alive: false}, {Destination: "dest3", Alive: true}, }}, - "dest1", true, + "dest3", true, }, { discovery.Matches{MatchType: discovery.MTProxy, Routes: []discovery.MatchedRoute{ @@ -406,14 +415,43 @@ func TestHttp_getMatch(t *testing.T) { }}, "", false, }, + { + discovery.Matches{MatchType: discovery.MTProxy, Routes: []discovery.MatchedRoute{}}, "", false, + }, } - h := Http{LBSelector: func(len int) int { return 0 }} + var count int32 + matcherMock := &MatcherMock{ + MatchFunc: func(srv string, src string) discovery.Matches { + return tbl[atomic.LoadInt32(&count)].matches + }, + } + + client := http.Client{} for i, tt := range tbl { t.Run(strconv.Itoa(i), func(t *testing.T) { - res, ok := h.getMatch(tt.matches) - require.Equal(t, tt.ok, ok) - assert.Equal(t, tt.res, res) + + h := Http{Matcher: matcherMock, LBSelector: func(len int) int { return 0 }} + handler := h.matchHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("req: %+v", r) + t.Logf("dst: %v", r.Context().Value(ctxURL)) + + v := r.Context().Value(ctxURL) + if v == nil { + require.False(t, tt.ok) + return + } + assert.Equal(t, tt.res, v.(*url.URL).String()) + })) + + req, err := http.NewRequest("GET", "http://example.com", nil) + require.NoError(t, err) + wr := httptest.NewRecorder() + handler.ServeHTTP(wr, req) + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) + atomic.AddInt32(&count, 1) }) } } diff --git a/examples/plugin/Dockerfile b/examples/plugin/Dockerfile new file mode 100644 index 0000000..0735db4 --- /dev/null +++ b/examples/plugin/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.16-alpine as build + +ENV GOFLAGS="-mod=vendor" +ENV CGO_ENABLED=0 + +ADD . /build +WORKDIR /build + +RUN go build -o /build/plugin-example -ldflags "-X main.revision=${version} -s -w" + + +FROM ghcr.io/umputun/baseimage/app:v1.6.1 as base + +FROM scratch + +COPY --from=build /build/plugin-example /srv/plugin-example + +WORKDIR /srv +ENTRYPOINT ["/srv/plugin-example"] diff --git a/examples/plugin/README.md b/examples/plugin/README.md new file mode 100644 index 0000000..ff19f7f --- /dev/null +++ b/examples/plugin/README.md @@ -0,0 +1,2 @@ +# Example of plugin + diff --git a/examples/plugin/docker-compose.yml b/examples/plugin/docker-compose.yml new file mode 100644 index 0000000..7b33f3d --- /dev/null +++ b/examples/plugin/docker-compose.yml @@ -0,0 +1,44 @@ +services: + reproxy: + image: umputun/reproxy:master + container_name: reproxy + hostname: reproxy + ports: + - "80:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./web:/web + environment: + - TZ=America/Chicago + - LISTEN=0.0.0.0:8080 + - DOCKER_ENABLED=true + - DOCKER_AUTO=true + - ASSETS_LOCATION=/web + - DEBUG=true + - PLUGIN_ENABLED=true + - PLUGIN_LISTEN=0.0.0.0:8081 + - HEADER= + X-Frame-Options:SAMEORIGIN, + X-XSS-Protection:1; mode=block;, + Content-Security-Policy:default-src 'self'; style-src 'self' 'unsafe-inline'; + + plugin-example: + build: . + container_name: plugin-example + hostname: plugin-example + + # automatic destination, will be mapped for ^/svc1/(.*) + svc1: + image: ghcr.io/umputun/echo-http + hostname: svc1 + container_name: svc1 + command: --message="hello world from svc1" + + + # automatic destination, will be mapped for ^/svc2/(.*) + svc2: + image: ghcr.io/umputun/echo-http + hostname: svc2 + container_name: svc2 + command: --message="hello world from svc2" + diff --git a/examples/plugin/go.mod b/examples/plugin/go.mod new file mode 100644 index 0000000..8b48a40 --- /dev/null +++ b/examples/plugin/go.mod @@ -0,0 +1,7 @@ +module github.com/umputun/reproxy/plugin + +go 1.16 + +require github.com/umputun/reproxy v0.6.0 + +replace github.com/umputun/reproxy => ../../ diff --git a/examples/plugin/go.sum b/examples/plugin/go.sum new file mode 100644 index 0000000..e24199d --- /dev/null +++ b/examples/plugin/go.sum @@ -0,0 +1,410 @@ +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/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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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/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/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/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +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/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/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +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/repeater v1.1.3 h1:q6+JQF14ESSy28Dd7F+wRelY4F+41HJ0LEy/szNnMiE= +github.com/go-pkgz/repeater v1.1.3/go.mod h1:hVTavuO5x3Gxnu8zW7d6sQBfAneKV8X2FjU48kGfpKw= +github.com/go-pkgz/rest v1.9.2/go.mod h1:wZ/dGipZUaF9to0vIQl7PwDHgWQDB0jsrFg1xnAKLDw= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +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/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/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/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/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +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/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +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/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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +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-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-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +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/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-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/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/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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +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/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= +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/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.18.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/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +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/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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/umputun/go-flags v1.5.1/go.mod h1:nTbvsO/hKqe7Utri/NoyN18GR3+EWf+9RrmsdwdhrEc= +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/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-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-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +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/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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +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/sync v0.0.0-20201207232520-09787c993a3a/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-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-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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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/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-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/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/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/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/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +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.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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/examples/plugin/main.go b/examples/plugin/main.go new file mode 100644 index 0000000..feec46a --- /dev/null +++ b/examples/plugin/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "context" + "log" + "net/http" + + "github.com/umputun/reproxy/lib" +) + +func main() { + + // create demo plugin on port 1234 with two methods: HeaderThing and ErrorThing + // both called via RPC from reproxy core with fully formed lib.Request + plugin := lib.Plugin{ + Name: "TestPlugin", + Address: "plugin-example:1234", + Methods: []string{"HeaderThing", "ErrorThing"}, + } + log.Printf("start demo plugin") + // Do starts the plugin listener and register with reproxy plugin conductor + if err := plugin.Do(context.TODO(), "http://reproxy:8081", new(Handler)); err != nil { + log.Fatal(err) + } +} + +// Handler is an example of middleware handler altering headers and stastus +type Handler struct{} + +// HeaderThing adds key:val header to the response +func (h *Handler) HeaderThing(req lib.Request, res *lib.Response) (err error) { + log.Printf("req: %+v", req) + res.HeadersOut = http.Header{} + res.HeadersOut.Add("key", "val") + res.HeadersIn = http.Header{} + res.HeadersIn.Add("token", "something") + res.StatusCode = 200 // each handler has to set status code + return +} + +// ErrorThing returns status 500 on "/fail" url. This terminated processing chain on reproxy side immediately +func (h *Handler) ErrorThing(req lib.Request, res *lib.Response) (err error) { + log.Printf("req: %+v", req) + if req.URL == "/fail" { + res.StatusCode = 500 + return + } + res.StatusCode = 200 + return +} diff --git a/examples/plugin/vendor/github.com/go-pkgz/lgr/.gitignore b/examples/plugin/vendor/github.com/go-pkgz/lgr/.gitignore new file mode 100644 index 0000000..6e85044 --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/lgr/.gitignore @@ -0,0 +1,13 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +vendor diff --git a/examples/plugin/vendor/github.com/go-pkgz/lgr/.golangci.yml b/examples/plugin/vendor/github.com/go-pkgz/lgr/.golangci.yml new file mode 100644 index 0000000..23fd915 --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/lgr/.golangci.yml @@ -0,0 +1,70 @@ +linters-settings: + govet: + check-shadowing: true + golint: + min-confidence: 0 + gocyclo: + min-complexity: 15 + maligned: + suggest-new: true + dupl: + threshold: 100 + goconst: + min-len: 2 + min-occurrences: 2 + misspell: + locale: US + lll: + line-length: 140 + gocritic: + enabled-tags: + - performance + - style + - experimental + disabled-checks: + - wrapperFunc + - hugeParam + +linters: + enable: + - megacheck + - golint + - govet + - unconvert + - megacheck + - structcheck + - gas + - gocyclo + - dupl + - misspell + - unparam + - varcheck + - deadcode + - typecheck + - ineffassign + - varcheck + - stylecheck + - gochecknoinits + - scopelint + - gocritic + - nakedret + - gosimple + - prealloc + fast: false + disable-all: true + +run: + output: + format: tab + skip-dirs: + - vendor + +issues: + exclude-rules: + - text: "should have a package comment, unless it's in another file for this package" + linters: + - golint + - text: "at least one file in a package should have a package comment" + linters: + - stylecheck + exclude-use-default: false diff --git a/examples/plugin/vendor/github.com/go-pkgz/lgr/LICENSE b/examples/plugin/vendor/github.com/go-pkgz/lgr/LICENSE new file mode 100644 index 0000000..c1b684b --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/lgr/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Umputun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/plugin/vendor/github.com/go-pkgz/lgr/README.md b/examples/plugin/vendor/github.com/go-pkgz/lgr/README.md new file mode 100644 index 0000000..f01f999 --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/lgr/README.md @@ -0,0 +1,124 @@ +# lgr - simple logger with some extras +[![Build Status](https://github.com/go-pkgz/lgr/workflows/build/badge.svg)](https://github.com/go-pkgz/lgr/actions) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/lgr/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/lgr?branch=master) [![godoc](https://godoc.org/github.com/go-pkgz/lgr?status.svg)](https://godoc.org/github.com/go-pkgz/lgr) + +## install + +`go get github.com/go-pkgz/lgr` + +## usage + +```go + l := lgr.New(lgr.Msec, lgr.Debug, lgr.CallerFile, lgr.CallerFunc) // allow debug and caller info, timestamp with milliseconds + l.Logf("INFO some important message, %v", err) + l.Logf("DEBUG some less important message, %v", err) +``` + +output looks like this: +``` +2018/01/07 13:02:34.000 INFO {svc/handler.go:101 h.MyFunc1} some important message, can't open file myfile.xyz +2018/01/07 13:02:34.015 DEBUG {svc/handler.go:155 h.MyFunc2} some less important message, file is too small` +``` + +_Without `lgr.Caller*` it will drop `{caller}` part_ + +## details + +### interfaces and default loggers + +- `lgr` package provides a single interface `lgr.L` with a single method `Logf(format string, args ...interface{})`. Function wrapper `lgr.Func` allows making `lgr.L` from a function directly. +- Default logger functionality can be used without `lgr.New` (see "global logger") +- Two predefined loggers available: `lgr.NoOp` (do-nothing logger) and `lgr.Std` (passing directly to stdlib log) + +### options + +`lgr.New` call accepts functional options: + +- `lgr.Debug` - turn debug mode on to allow messages with "DEBUG" level (filtered otherwise) +- `lgr.Trace` - turn trace mode on to allow messages with "TRACE" abd "DEBUG" levels both (filtered otherwise) +- `lgr.Out(io.Writer)` - sets the output writer, default `os.Stdout` +- `lgr.Err(io.Writer)` - sets the error writer, default `os.Stderr` +- `lgr.CallerFile` - adds the caller file info +- `lgr.CallerFunc` - adds the caller function info +- `lgr.CallerPkg` - adds the caller package +- `lgr.LevelBraces` - wraps levels with "[" and "]" +- `lgr.Msec` - adds milliseconds to timestamp +- `lgr.Format` - sets a custom template, overwrite all other formatting modifiers. +- `lgr.Secret(secret ...)` - sets list of the secrets to hide from the logging outputs. +- `lgr.Map(mapper)` - sets mapper functions to change elements of the logging output based on levels. +- `lgr.StackTraceOnError` - turns on stack trace for ERROR level. + +example: `l := lgr.New(lgr.Debug, lgr.Msec)` + +#### formatting templates: + +Several predefined templates provided and can be passed directly to `lgr.Format`, i.e. `lgr.Format(lgr.WithMsec)` + +``` + Short = `{{.DT.Format "2006/01/02 15:04:05"}} {{.Level}} {{.Message}}` + WithMsec = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} {{.Message}}` + WithPkg = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerPkg}}) {{.Message}}` + ShortDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFile}}:{{.CallerLine}}) {{.Message}}` + FuncDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFunc}}) {{.Message}}` + FullDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFile}}:{{.CallerLine}} {{.CallerFunc}}) {{.Message}}` +``` + +User can make a custom template and pass it directly to `lgr.Format`. For example: + +```go + lgr.Format(`{{.Level}} - {{.DT.Format "2006-01-02T15:04:05Z07:00"}} - {{.CallerPkg}} - {{.Message}}`) +``` + +_Note: formatter (predefined or custom) adds measurable overhead - the cost will depend on the version of Go, but is between 30 + and 50% in recent tests with 1.12. You can validate this in your environment via benchmarks: `go test -bench=. -run=Bench`_ + +### levels + +`lgr.Logf` recognize prefixes like `INFO` or `[INFO]` as levels. The full list of supported levels - `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `PANIC` and `FATAL`. + +- `TRACE` will be filtered unless `lgr.Trace` option defined +- `DEBUG` will be filtered unless `lgr.Debug` or `lgr.Trace` options defined +- `INFO` and `WARN` don't have any special behavior attached +- `ERROR` sends messages to both out and err writers +- `FATAL` and send messages to both out and err writers and exit(1) +- `PANIC` does the same as `FATAL` but in addition sends dump of callers and runtime info to err. + +### mapper + +Elements of the output can be altered with a set of user defined function passed as `lgr.Map` options. Such a mapper changes +the value of an element (i.e. timestamp, level, message, caller) and has separate functions for each level. Note: both level +and messages elements handled by the same function for a given level. + +_A typical use-case is to produce colorful output with a user-define colorization library._ + +example with [fatih/color](https://github.com/fatih/color): + +```go + colorizer := lgr.Mapper{ + ErrorFunc: func(s string) string { return color.New(color.FgHiRed).Sprint(s) }, + WarnFunc: func(s string) string { return color.New(color.FgHiYellow).Sprint(s) }, + InfoFunc: func(s string) string { return color.New(color.FgHiWhite).Sprint(s) }, + DebugFunc: func(s string) string { return color.New(color.FgWhite).Sprint(s) }, + CallerFunc: func(s string) string { return color.New(color.FgBlue).Sprint(s) }, + TimeFunc: func(s string) string { return color.New(color.FgCyan).Sprint(s) }, + } + + logOpts := []lgr.Option{lgr.Msec, lgr.LevelBraces, lgr.Map(colorizer)} +``` +### adaptors + +`lgr` logger can be converted to `io.Writer` or `*log.Logger` + +- `lgr.ToWriter(l lgr.L, level string) io.Writer` - makes io.Writer forwarding write ops to underlying `lgr.L` +- `lgr.ToStdLogger(l lgr.L, level string) *log.Logger` - makes standard logger on top of `lgr.L` + +_`level` parameter is optional, if defined (non-empty) will enforce the level._ + +- `lgr.SetupStdLogger(opts ...Option)` initializes std global logger (`log.std`) with lgr logger and given options. +All standard methods like `log.Print`, `log.Println`, `log.Fatal` and so on will be forwarder to lgr. + +### global logger + +Users **should avoid** global logger and pass the concrete logger as a dependency. However, in some cases a global logger may be needed, for example migration from stdlib `log` to `lgr`. For such cases `log "github.com/go-pkgz/lgr"` can be imported instead of `log` package. + +Global logger provides `lgr.Printf`, `lgr.Print` and `lgr.Fatalf` functions. User can customize the logger by calling `lgr.Setup(options ...)`. The instance of this logger can be retrieved with `lgr.Default()` + diff --git a/examples/plugin/vendor/github.com/go-pkgz/lgr/adaptor.go b/examples/plugin/vendor/github.com/go-pkgz/lgr/adaptor.go new file mode 100644 index 0000000..03e80eb --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/lgr/adaptor.go @@ -0,0 +1,41 @@ +package lgr + +import ( + "log" + "strings" +) + +// Writer holds lgr.L and wraps with io.Writer interface +type Writer struct { + L + level string // if defined added to each message +} + +// Write to lgr.L +func (w *Writer) Write(p []byte) (n int, err error) { + w.Logf(w.level + string(p)) + return len(p), nil +} + +// ToWriter makes io.Writer for given lgr.L with optional level +func ToWriter(l L, level string) *Writer { + if level != "" && !strings.HasSuffix(level, " ") { + level += " " + } + return &Writer{l, level} +} + +// ToStdLogger makes standard logger +func ToStdLogger(l L, level string) *log.Logger { + return log.New(ToWriter(l, level), "", 0) +} + +// SetupStdLogger makes the default std logger with lgr.L +func SetupStdLogger(opts ...Option) { + logOpts := append([]Option{CallerDepth(3)}, opts...) // skip 3 more frames to compensate stdlog calls + l := New(logOpts...) + l.reTrace = reTraceStd // std logger split on log/ path + log.SetOutput(ToWriter(l, "")) + log.SetPrefix("") + log.SetFlags(0) +} diff --git a/examples/plugin/vendor/github.com/go-pkgz/lgr/go.mod b/examples/plugin/vendor/github.com/go-pkgz/lgr/go.mod new file mode 100644 index 0000000..4ea4d22 --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/lgr/go.mod @@ -0,0 +1,5 @@ +module github.com/go-pkgz/lgr + +require github.com/stretchr/testify v1.6.1 + +go 1.15 diff --git a/examples/plugin/vendor/github.com/go-pkgz/lgr/go.sum b/examples/plugin/vendor/github.com/go-pkgz/lgr/go.sum new file mode 100644 index 0000000..1f1e7af --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/lgr/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/examples/plugin/vendor/github.com/go-pkgz/lgr/interface.go b/examples/plugin/vendor/github.com/go-pkgz/lgr/interface.go new file mode 100644 index 0000000..46b961a --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/lgr/interface.go @@ -0,0 +1,48 @@ +package lgr + +import ( + stdlog "log" +) + +var def = New() // default logger doesn't allow DEBUG and doesn't add caller info + +// L defines minimal interface used to log things +type L interface { + Logf(format string, args ...interface{}) +} + +// Func type is an adapter to allow the use of ordinary functions as Logger. +type Func func(format string, args ...interface{}) + +// Logf calls f(format, args...) +func (f Func) Logf(format string, args ...interface{}) { f(format, args...) } + +// NoOp logger +var NoOp = Func(func(format string, args ...interface{}) {}) + +// Std logger sends to std default logger directly +var Std = Func(func(format string, args ...interface{}) { stdlog.Printf(format, args...) }) + +// Printf simplifies replacement of std logger +func Printf(format string, args ...interface{}) { + def.logf(format, args...) +} + +// Print simplifies replacement of std logger +func Print(line string) { + def.logf(line) +} + +// Fatalf simplifies replacement of std logger +func Fatalf(format string, args ...interface{}) { + def.logf(format, args...) + def.fatal() +} + +// Setup default logger with options +func Setup(opts ...Option) { + def = New(opts...) +} + +// Default returns pre-constructed def logger (debug off, callers disabled) +func Default() L { return def } diff --git a/examples/plugin/vendor/github.com/go-pkgz/lgr/logger.go b/examples/plugin/vendor/github.com/go-pkgz/lgr/logger.go new file mode 100644 index 0000000..9ba12d3 --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/lgr/logger.go @@ -0,0 +1,407 @@ +// Package lgr provides a simple logger with some extras. Primary way to log is Logf method. +// The logger's output can be customized in 2 ways: +// - by setting individual formatting flags, i.e. lgr.New(lgr.Msec, lgr.CallerFunc) +// - by passing formatting template, i.e. lgr.New(lgr.Format(lgr.Short)) +// Leveled output works for messages based on text prefix, i.e. Logf("INFO some message") means INFO level. +// Debug and trace levels can be filtered based on lgr.Trace and lgr.Debug options. +// ERROR, FATAL and PANIC levels send to err as well. FATAL terminate caller application with os.Exit(1) +// and PANIC also prints stack trace. +package lgr + +import ( + "bytes" + "fmt" + "io" + "os" + "path" + "regexp" + "runtime" + "strconv" + "strings" + "sync" + "text/template" + "time" +) + +var levels = []string{"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "PANIC", "FATAL"} + +const ( + // Short logging format + Short = `{{.DT.Format "2006/01/02 15:04:05"}} {{.Level}} {{.Message}}` + // WithMsec is a logging format with milliseconds + WithMsec = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} {{.Message}}` + // WithPkg is WithMsec logging format with caller package + WithPkg = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerPkg}}) {{.Message}}` + // ShortDebug is WithMsec logging format with caller file and line + ShortDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFile}}:{{.CallerLine}}) {{.Message}}` + // FuncDebug is WithMsec logging format with caller function + FuncDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFunc}}) {{.Message}}` + // FullDebug is WithMsec logging format with caller file, line and function + FullDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFile}}:{{.CallerLine}} {{.CallerFunc}}) {{.Message}}` +) + +var secretReplacement = []byte("******") + +var ( + reTraceDefault = regexp.MustCompile(`.*/lgr/logger\.go.*\n`) + reTraceStd = regexp.MustCompile(`.*/log/log\.go.*\n`) +) + +// Logger provided simple logger with basic support of levels. Thread safe +type Logger struct { + // set with Option calls + stdout, stderr io.Writer // destination writes for out and err + dbg bool // allows reporting for DEBUG level + trace bool // allows reporting for TRACE and DEBUG levels + callerFile bool // reports caller file with line number, i.e. foo/bar.go:89 + callerFunc bool // reports caller function name, i.e. bar.myFunc + callerPkg bool // reports caller package name + levelBraces bool // encloses level with [], i.e. [INFO] + callerDepth int // how many stack frames to skip, relative to the real (reported) frame + format string // layout template + secrets [][]byte // sub-strings to secrets by matching + mapper Mapper // map (alter) output based on levels + + // internal use + now nowFn + fatal panicFn + msec bool + lock sync.Mutex + callerOn bool + levelBracesOn bool + errorDump bool + templ *template.Template + reTrace *regexp.Regexp +} + +// can be redefined internally for testing +type nowFn func() time.Time +type panicFn func() + +// layout holds all parts to construct the final message with template or with individual flags +type layout struct { + DT time.Time + Level string + Message string + CallerPkg string + CallerFile string + CallerFunc string + CallerLine int +} + +// New makes new leveled logger. By default writes to stdout/stderr. +// default format: 2018/01/07 13:02:34.123 DEBUG some message 123 +func New(options ...Option) *Logger { + + res := Logger{ + now: time.Now, + fatal: func() { os.Exit(1) }, + stdout: os.Stdout, + stderr: os.Stderr, + callerDepth: 0, + mapper: nopMapper, + reTrace: reTraceDefault, + } + for _, opt := range options { + opt(&res) + } + + if res.format != "" { + // formatter defined + var err error + res.templ, err = template.New("lgr").Parse(res.format) + if err != nil { + fmt.Printf("invalid template %s, error %v. switched to %s\n", res.format, err, Short) + res.format = Short + res.templ = template.Must(template.New("lgrDefault").Parse(Short)) + } + + buf := bytes.Buffer{} + if err = res.templ.Execute(&buf, layout{}); err != nil { + fmt.Printf("failed to execute template %s, error %v. switched to %s\n", res.format, err, Short) + res.format = Short + res.templ = template.Must(template.New("lgrDefault").Parse(Short)) + } + } + + // set *On flags once for optimization on multiple Logf calls + res.callerOn = strings.Contains(res.format, "{{.Caller") || res.callerFile || res.callerFunc || res.callerPkg + res.levelBracesOn = strings.Contains(res.format, "[{{.Level}}]") || res.levelBraces + + return &res +} + +// Logf implements L interface to output with printf style. +// DEBUG and TRACE filtered out by dbg and trace flags. +// ERROR and FATAL also send the same line to err writer. +// FATAL and PANIC adds runtime stack and os.exit(1), like panic. +func (l *Logger) Logf(format string, args ...interface{}) { + // to align call depth between (*Logger).Logf() and, for example, Printf() + l.logf(format, args...) +} + +//nolint gocyclo +func (l *Logger) logf(format string, args ...interface{}) { + + var lv, msg string + if len(args) == 0 { + lv, msg = l.extractLevel(format) + } else { + lv, msg = l.extractLevel(fmt.Sprintf(format, args...)) + } + + if lv == "DEBUG" && !l.dbg { + return + } + if lv == "TRACE" && !l.trace { + return + } + + var ci callerInfo + if l.callerOn { // optimization to avoid expensive caller evaluation if caller info not in the template + ci = l.reportCaller(l.callerDepth) + } + + elems := layout{ + DT: l.now(), + Level: l.formatLevel(lv), + Message: strings.TrimSuffix(msg, "\n"), // output adds EOL, trim from the message if passed + CallerFunc: ci.FuncName, + CallerFile: ci.File, + CallerPkg: ci.Pkg, + CallerLine: ci.Line, + } + + var data []byte + if l.format == "" { + data = []byte(l.formatWithOptions(elems)) + } else { + buf := bytes.Buffer{} + err := l.templ.Execute(&buf, elems) // once constructed, a template may be executed safely in parallel. + if err != nil { + fmt.Printf("failed to execute template, %v\n", err) // should never happen + } + data = buf.Bytes() + } + data = append(data, '\n') + + if l.levelBracesOn { // rearrange space in short levels + data = bytes.Replace(data, []byte("[WARN ]"), []byte("[WARN] "), 1) + data = bytes.Replace(data, []byte("[INFO ]"), []byte("[INFO] "), 1) + } + data = l.hideSecrets(data) + + l.lock.Lock() + _, _ = l.stdout.Write(data) + + // write to err as well for high levels, exit(1) on fatal and panic and dump stack on panic level + switch lv { + case "ERROR": + if l.stderr != l.stdout { + _, _ = l.stderr.Write(data) + } + if l.errorDump { + stackInfo := make([]byte, 1024*1024) + if stackSize := runtime.Stack(stackInfo, false); stackSize > 0 { + traceLines := l.reTrace.Split(string(stackInfo[:stackSize]), -1) + if len(traceLines) > 0 { + _, _ = l.stdout.Write([]byte(">>> stack trace:\n" + traceLines[len(traceLines)-1])) + } + } + } + case "FATAL": + if l.stderr != l.stdout { + _, _ = l.stderr.Write(data) + } + l.fatal() + case "PANIC": + if l.stderr != l.stdout { + _, _ = l.stderr.Write(data) + } + _, _ = l.stderr.Write(getDump()) + l.fatal() + } + + l.lock.Unlock() +} + +func (l *Logger) hideSecrets(data []byte) []byte { + for _, h := range l.secrets { + data = bytes.Replace(data, h, secretReplacement, -1) + } + return data +} + +type callerInfo struct { + File string + Line int + FuncName string + Pkg string +} + +// calldepth 0 identifying the caller of reportCaller() +func (l *Logger) reportCaller(calldepth int) (res callerInfo) { + + // caller gets file, line number abd function name via runtime.Callers + // file looks like /go/src/github.com/go-pkgz/lgr/logger.go + // file is an empty string if not known. + // funcName looks like: + // main.Test + // foo/bar.Test + // foo/bar.Test.func1 + // foo/bar.(*Bar).Test + // foo/bar.glob..func1 + // funcName is an empty string if not known. + // line is a zero if not known. + caller := func(calldepth int) (file string, line int, funcName string) { + pcs := make([]uintptr, 1) + n := runtime.Callers(calldepth, pcs) + if n != 1 { + return "", 0, "" + } + + frame, _ := runtime.CallersFrames(pcs).Next() + + return frame.File, frame.Line, frame.Function + } + + // add 5 to adjust stack level because it was called from 3 nested functions added by lgr, i.e. caller, + // reportCaller and logf, plus 2 frames by runtime + filePath, line, funcName := caller(calldepth + 2 + 3) + if (filePath == "") || (line <= 0) || (funcName == "") { + return callerInfo{} + } + + _, pkgInfo := path.Split(path.Dir(filePath)) + res.Pkg = strings.Split(pkgInfo, "@")[0] // remove version from package name + + res.File = filePath + if pathElems := strings.Split(filePath, "/"); len(pathElems) > 2 { + res.File = strings.Join(pathElems[len(pathElems)-2:], "/") + } + res.Line = line + + funcNameElems := strings.Split(funcName, "/") + res.FuncName = funcNameElems[len(funcNameElems)-1] + + return res +} + +// speed-optimized version of formatter, used with individual options only, i.e. without Format call +func (l *Logger) formatWithOptions(elems layout) (res string) { + + orElse := func(flag bool, fnTrue func() string, fnFalse func() string) string { + if flag { + return fnTrue() + } + return fnFalse() + } + nothing := func() string { return "" } + + parts := make([]string, 0, 4) + + parts = append( + parts, + l.mapper.TimeFunc(orElse(l.msec, + func() string { return elems.DT.Format("2006/01/02 15:04:05.000") }, + func() string { return elems.DT.Format("2006/01/02 15:04:05") }, + )), + l.levelMapper(elems.Level)(orElse(l.levelBraces, + func() string { return `[` + elems.Level + `]` }, + func() string { return elems.Level }, + )), + ) + + if l.callerFile || l.callerFunc || l.callerPkg { + var callerParts []string + v := orElse(l.callerFile, func() string { return elems.CallerFile + ":" + strconv.Itoa(elems.CallerLine) }, nothing) + if v != "" { + callerParts = append(callerParts, v) + } + if v := orElse(l.callerFunc, func() string { return elems.CallerFunc }, nothing); v != "" { + callerParts = append(callerParts, v) + } + if v := orElse(l.callerPkg, func() string { return elems.CallerPkg }, nothing); v != "" { + callerParts = append(callerParts, v) + } + + caller := "{" + strings.Join(callerParts, " ") + "}" + if l.mapper.CallerFunc != nil { + caller = l.mapper.CallerFunc(caller) + } + parts = append(parts, caller) + } + + msg := elems.Message + if l.mapper.MessageFunc != nil { + msg = l.mapper.MessageFunc(elems.Message) + } + + parts = append(parts, l.levelMapper(elems.Level)(msg)) + return strings.Join(parts, " ") +} + +// formatLevel aligns level to 5 chars +func (l *Logger) formatLevel(lv string) string { + + spaces := "" + if len(lv) == 4 { + spaces = " " + } + return lv + spaces +} + +// extractLevel parses messages with optional level prefix and returns level and the message with stripped level +func (l *Logger) extractLevel(line string) (level, msg string) { + for _, lv := range levels { + if strings.HasPrefix(line, lv) { + return lv, strings.TrimSpace(line[len(lv):]) + } + if strings.HasPrefix(line, "["+lv+"]") { + return lv, strings.TrimSpace(line[len("["+lv+"]"):]) + } + } + return "INFO", line +} + +func (l *Logger) levelMapper(level string) mapFunc { + + nop := func(s string) string { + return s + } + + switch level { + case "TRACE", "DEBUG": + if l.mapper.DebugFunc == nil { + return nop + } + return l.mapper.DebugFunc + case "INFO ": + if l.mapper.InfoFunc == nil { + return nop + } + return l.mapper.InfoFunc + case "WARN ": + if l.mapper.WarnFunc == nil { + return nop + } + return l.mapper.WarnFunc + case "ERROR", "PANIC", "FATAL": + if l.mapper.ErrorFunc == nil { + return nop + } + return l.mapper.ErrorFunc + } + return func(s string) string { return s } +} + +// getDump reads runtime stack and returns as a string +func getDump() []byte { + maxSize := 5 * 1024 * 1024 + stacktrace := make([]byte, maxSize) + length := runtime.Stack(stacktrace, true) + if length > maxSize { + length = maxSize + } + return stacktrace[:length] +} diff --git a/examples/plugin/vendor/github.com/go-pkgz/lgr/mapper.go b/examples/plugin/vendor/github.com/go-pkgz/lgr/mapper.go new file mode 100644 index 0000000..c7bcc6e --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/lgr/mapper.go @@ -0,0 +1,28 @@ +package lgr + +// Mapper defines optional functions to change elements of the logged message for each part, based on levels. +// Only some mapFunc can be defined, by default does nothing. Can be used to alter the output, for example making some +// part of the output colorful. +type Mapper struct { + MessageFunc mapFunc // message mapper on all levels + ErrorFunc mapFunc // message mapper on ERROR level + WarnFunc mapFunc // message mapper on WARN level + InfoFunc mapFunc // message mapper on INFO level + DebugFunc mapFunc // message mapper on DEBUG level + + CallerFunc mapFunc // caller mapper, all levels + TimeFunc mapFunc // time mapper, all levels +} + +type mapFunc func(string) string + +// nopMapper is a default, doing nothing +var nopMapper = Mapper{ + MessageFunc: func(s string) string { return s }, + ErrorFunc: func(s string) string { return s }, + WarnFunc: func(s string) string { return s }, + InfoFunc: func(s string) string { return s }, + DebugFunc: func(s string) string { return s }, + CallerFunc: func(s string) string { return s }, + TimeFunc: func(s string) string { return s }, +} diff --git a/examples/plugin/vendor/github.com/go-pkgz/lgr/options.go b/examples/plugin/vendor/github.com/go-pkgz/lgr/options.go new file mode 100644 index 0000000..646d486 --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/lgr/options.go @@ -0,0 +1,92 @@ +package lgr + +import "io" + +// Option func type +type Option func(l *Logger) + +// Out sets out writer, stdout by default +func Out(w io.Writer) Option { + return func(l *Logger) { + l.stdout = w + } +} + +// Err sets error writer, stderr by default +func Err(w io.Writer) Option { + return func(l *Logger) { + l.stderr = w + } +} + +// Debug turn on dbg mode +func Debug(l *Logger) { + l.dbg = true +} + +// Trace turn on trace + dbg mode +func Trace(l *Logger) { + l.dbg = true + l.trace = true +} + +// CallerDepth sets number of stack frame skipped for caller reporting, 0 by default +func CallerDepth(n int) Option { + return func(l *Logger) { + l.callerDepth = n + } +} + +// Format sets output layout, overwrites all options for individual parts, i.e. Caller*, Msec and LevelBraces +func Format(f string) Option { + return func(l *Logger) { + l.format = f + } +} + +// CallerFunc adds caller info with function name. Ignored if Format option used. +func CallerFunc(l *Logger) { + l.callerFunc = true +} + +// CallerPkg adds caller's package name. Ignored if Format option used. +func CallerPkg(l *Logger) { + l.callerPkg = true +} + +// LevelBraces surrounds level with [], i.e. [INFO]. Ignored if Format option used. +func LevelBraces(l *Logger) { + l.levelBraces = true +} + +// CallerFile adds caller info with file, and line number. Ignored if Format option used. +func CallerFile(l *Logger) { + l.callerFile = true +} + +// Msec adds .msec to timestamp. Ignored if Format option used. +func Msec(l *Logger) { + l.msec = true +} + +// Secret sets list of substring to be hidden, i.e. replaced by "******" +// Useful to prevent passwords or other sensitive tokens to be logged. +func Secret(vals ...string) Option { + return func(l *Logger) { + for _, v := range vals { + l.secrets = append(l.secrets, []byte(v)) + } + } +} + +// Map sets mapper functions to change elements of the logged message based on levels. +func Map(m Mapper) Option { + return func(l *Logger) { + l.mapper = m + } +} + +// StackTraceOnError turns on stack trace for ERROR level. +func StackTraceOnError(l *Logger) { + l.errorDump = true +} diff --git a/examples/plugin/vendor/github.com/go-pkgz/repeater/.gitignore b/examples/plugin/vendor/github.com/go-pkgz/repeater/.gitignore new file mode 100644 index 0000000..f1c181e --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/repeater/.gitignore @@ -0,0 +1,12 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out diff --git a/examples/plugin/vendor/github.com/go-pkgz/repeater/.golangci.yml b/examples/plugin/vendor/github.com/go-pkgz/repeater/.golangci.yml new file mode 100644 index 0000000..989af86 --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/repeater/.golangci.yml @@ -0,0 +1,60 @@ +linters-settings: + govet: + check-shadowing: true + golint: + min-confidence: 0 + gocyclo: + min-complexity: 15 + maligned: + suggest-new: true + dupl: + threshold: 100 + goconst: + min-len: 2 + min-occurrences: 2 + misspell: + locale: US + lll: + line-length: 140 + gocritic: + enabled-tags: + - performance + - style + - experimental + disabled-checks: + - wrapperFunc + +linters: + disable-all: true + enable: + - megacheck + - govet + - unconvert + - megacheck + - structcheck + - gas + - gocyclo + - dupl + - misspell + - unparam + - varcheck + - deadcode + - typecheck + - ineffassign + - varcheck + fast: false + + +run: +# modules-download-mode: vendor + skip-dirs: + - vendor + +issues: + exclude-rules: + - text: "weak cryptographic primitive" + linters: + - gosec + +service: + golangci-lint-version: 1.16.x \ No newline at end of file diff --git a/examples/plugin/vendor/github.com/go-pkgz/repeater/.travis.yml b/examples/plugin/vendor/github.com/go-pkgz/repeater/.travis.yml new file mode 100644 index 0000000..697ce97 --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/repeater/.travis.yml @@ -0,0 +1,20 @@ +language: go + +go: + - "1.12.x" + +install: true + +before_install: + - export TZ=America/Chicago + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.13.2 + - go get github.com/mattn/goveralls + - export PATH=$(pwd)/bin:$PATH + +script: + - GO111MODULE=on go get ./... + - GO111MODULE=on go mod vendor + - GO111MODULE=on go test -v -mod=vendor -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1; + - GO111MODULE=on go test -v -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1; + - golangci-lint run || travis_terminate 1; + - $GOPATH/bin/goveralls -coverprofile=profile.cov -service=travis-ci diff --git a/examples/plugin/vendor/github.com/go-pkgz/repeater/LICENSE b/examples/plugin/vendor/github.com/go-pkgz/repeater/LICENSE new file mode 100644 index 0000000..ac54025 --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/repeater/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Umputun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/plugin/vendor/github.com/go-pkgz/repeater/README.md b/examples/plugin/vendor/github.com/go-pkgz/repeater/README.md new file mode 100644 index 0000000..76d8c90 --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/repeater/README.md @@ -0,0 +1,35 @@ +# Repeater [![Build Status](https://travis-ci.org/go-pkgz/repeater.svg?branch=master)](https://travis-ci.org/go-pkgz/repeater) [![Go Report Card](https://goreportcard.com/badge/github.com/go-pkgz/repeater)](https://goreportcard.com/report/github.com/go-pkgz/repeater) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/repeater/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/repeater?branch=master) + +Repeater calls a function until it returns no error, up to some number of iterations and delays defined by strategy. It terminates immediately on err from the provided (optional) list of critical errors. + +## Install and update + +`go get -u github.com/go-pkgz/repeater` + +## How to use + +New Repeater created by `New(strtg strategy.Interface)` or shortcut for defaults - `NewDefault(repeats int, delay time.Duration) *Repeater`. + +To activate invoke `Do` method. `Do` repeats func until no error returned. Predefined (optional) errors terminates the loop immediately. + +`func (r Repeater) Do(ctx context.Context, fun func() error, errors ...error) (err error)` + +### Repeating strategy + +User can provide his own strategy implementing the interface: + +```go +type Interface interface { + Start(ctx context.Context) chan struct{} +} +``` + +Returned channels used as "ticks," i.e., for each repeat or initial operation one read from this channel needed. Closing this channel indicates "done with retries." It is pretty much the same idea as `time.Timer` or `time.Tick` implements. Note - the first (technically not-repeated-yet) call won't happen **until something sent to the channel**. For this reason, the typical strategy sends the first "tick" before the first wait/sleep. + +Three most common strategies provided by package and ready to use: +1. **Fixed delay**, up to max number of attempts - `NewFixedDelay(repeats int, delay time.Duration)`. +It is the default strategy used by `repeater.NewDefault` constructor +2. **BackOff** with jitter provides exponential backoff. It starts from 100ms interval and goes in steps with `last * math.Pow(factor, attempt)`. Optional jitter randomizes intervals a little bit. The strategy created by `NewBackoff(repeats int, factor float64, jitter bool)`. _Factor = 1 effectively makes this strategy fixed with 100ms delay._ +3. **Once** strategy does not do any repeats and mainly used for tests/mocks - `NewOnce()` + + diff --git a/examples/plugin/vendor/github.com/go-pkgz/repeater/go.mod b/examples/plugin/vendor/github.com/go-pkgz/repeater/go.mod new file mode 100644 index 0000000..3488d7b --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/repeater/go.mod @@ -0,0 +1,5 @@ +module github.com/go-pkgz/repeater + +go 1.12 + +require github.com/stretchr/testify v1.3.0 diff --git a/examples/plugin/vendor/github.com/go-pkgz/repeater/go.sum b/examples/plugin/vendor/github.com/go-pkgz/repeater/go.sum new file mode 100644 index 0000000..4347755 --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/repeater/go.sum @@ -0,0 +1,7 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/examples/plugin/vendor/github.com/go-pkgz/repeater/repeater.go b/examples/plugin/vendor/github.com/go-pkgz/repeater/repeater.go new file mode 100644 index 0000000..e6370ce --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/repeater/repeater.go @@ -0,0 +1,64 @@ +// Package repeater call fun till it returns no error, up to repeat some number of iterations and delays defined by strategy. +// Repeats number and delays defined by strategy.Interface. Terminates immediately on err from +// provided, optional list of critical errors +package repeater + +import ( + "context" + "time" + + "github.com/go-pkgz/repeater/strategy" +) + +// Repeater is the main object, should be made by New or NewDefault, embeds strategy +type Repeater struct { + strategy.Interface +} + +// New repeater with a given strategy. If strategy=nil initializes with FixedDelay 5sec, 10 times. +func New(strtg strategy.Interface) *Repeater { + if strtg == nil { + strtg = &strategy.FixedDelay{Repeats: 10, Delay: time.Second * 5} + } + result := Repeater{Interface: strtg} + return &result +} + +// NewDefault makes repeater with FixedDelay strategy +func NewDefault(repeats int, delay time.Duration) *Repeater { + return New(&strategy.FixedDelay{Repeats: repeats, Delay: delay}) +} + +// Do repeats fun till no error. Predefined (optional) errors terminate immediately +func (r Repeater) Do(ctx context.Context, fun func() error, errors ...error) (err error) { + + ctx, cancelFunc := context.WithCancel(ctx) + defer cancelFunc() // ensure strategy's channel termination + + inErrors := func(err error) bool { + for _, e := range errors { + if e == err { + return true + } + } + return false + } + + ch := r.Start(ctx) // channel of ticks-like events provided by strategy + for { + select { + case <-ctx.Done(): + return ctx.Err() + case _, ok := <-ch: + if !ok { // closed channel indicates completion or early termination, set by strategy + return err + } + if err = fun(); err == nil { + return nil + } + if err != nil && inErrors(err) { // terminate on critical error from provided list + return err + } + } + } +} diff --git a/examples/plugin/vendor/github.com/go-pkgz/repeater/strategy/backoff.go b/examples/plugin/vendor/github.com/go-pkgz/repeater/strategy/backoff.go new file mode 100644 index 0000000..12f53b6 --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/repeater/strategy/backoff.go @@ -0,0 +1,59 @@ +package strategy + +import ( + "context" + "math" + "math/rand" + "sync" + "time" +) + +// Backoff implements strategy.Interface for exponential-backoff +// it starts from 100ms (by default, if no Duration set) and goes in steps with last * math.Pow(factor, attempt) +// optional jitter randomize intervals a little bit. +type Backoff struct { + Duration time.Duration + Repeats int + Factor float64 + Jitter bool + + once sync.Once +} + +// Start returns channel, similar to time.Timer +// then publishing signals to channel ch for retries attempt. Closed ch indicates "done" event +// consumer (repeater) should stop it explicitly after completion +func (b *Backoff) Start(ctx context.Context) <-chan struct{} { + + b.once.Do(func() { + if b.Duration == 0 { + b.Duration = 100 * time.Millisecond + } + if b.Repeats == 0 { + b.Repeats = 1 + } + if b.Factor <= 0 { + b.Factor = 1 + } + }) + + ch := make(chan struct{}) + go func() { + defer close(ch) + rnd := rand.New(rand.NewSource(int64(time.Now().Nanosecond()))) + for i := 0; i < b.Repeats; i++ { + select { + case <-ctx.Done(): + return + case ch <- struct{}{}: + } + + delay := float64(b.Duration) * math.Pow(b.Factor, float64(i)) + if b.Jitter { + delay = rnd.Float64()*(float64(2*b.Duration)) + (delay - float64(b.Duration)) + } + sleep(ctx, time.Duration(delay)) + } + }() + return ch +} diff --git a/examples/plugin/vendor/github.com/go-pkgz/repeater/strategy/fixed.go b/examples/plugin/vendor/github.com/go-pkgz/repeater/strategy/fixed.go new file mode 100644 index 0000000..ddd283e --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/repeater/strategy/fixed.go @@ -0,0 +1,36 @@ +package strategy + +import ( + "context" + "time" +) + +// FixedDelay implements strategy.Interface for fixed intervals up to max repeats +type FixedDelay struct { + Repeats int + Delay time.Duration +} + +// Start returns channel, similar to time.Timer +// then publishing signals to channel ch for retries attempt. +// can be terminated (canceled) via context. +func (s *FixedDelay) Start(ctx context.Context) <-chan struct{} { + if s.Repeats == 0 { + s.Repeats = 1 + } + ch := make(chan struct{}) + go func() { + defer func() { + close(ch) + }() + for i := 0; i < s.Repeats; i++ { + select { + case <-ctx.Done(): + return + case ch <- struct{}{}: + } + sleep(ctx, s.Delay) + } + }() + return ch +} diff --git a/examples/plugin/vendor/github.com/go-pkgz/repeater/strategy/strategy.go b/examples/plugin/vendor/github.com/go-pkgz/repeater/strategy/strategy.go new file mode 100644 index 0000000..75af12b --- /dev/null +++ b/examples/plugin/vendor/github.com/go-pkgz/repeater/strategy/strategy.go @@ -0,0 +1,35 @@ +// Package strategy defines repeater's strategy and implements some. +// Strategy result is a channel acting like time.Timer ot time.Tick +package strategy + +import ( + "context" + "time" +) + +// Interface for repeater strategy. Returns channel with ticks +type Interface interface { + Start(ctx context.Context) <-chan struct{} +} + +// Once strategy eliminate repeats and makes a single try only +type Once struct{} + +// Start returns closed channel with a single element to prevent any repeats +func (s *Once) Start(ctx context.Context) <-chan struct{} { + ch := make(chan struct{}) + go func() { + ch <- struct{}{} + close(ch) + }() + return ch +} + +func sleep(ctx context.Context, duration time.Duration) { + select { + case <-time.After(duration): + return + case <-ctx.Done(): + return + } +} diff --git a/examples/plugin/vendor/github.com/umputun/reproxy/LICENSE b/examples/plugin/vendor/github.com/umputun/reproxy/LICENSE new file mode 100644 index 0000000..682ea0c --- /dev/null +++ b/examples/plugin/vendor/github.com/umputun/reproxy/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Umputun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/plugin/vendor/github.com/umputun/reproxy/lib/plugin.go b/examples/plugin/vendor/github.com/umputun/reproxy/lib/plugin.go new file mode 100644 index 0000000..2e287f2 --- /dev/null +++ b/examples/plugin/vendor/github.com/umputun/reproxy/lib/plugin.go @@ -0,0 +1,105 @@ +package lib + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "net/rpc" + "time" + + log "github.com/go-pkgz/lgr" + "github.com/go-pkgz/repeater" +) + +// Plugin provides cancelable rpc server used to run custom plugins +type Plugin struct { + Name string `json:"name"` + Address string `json:"address"` + Methods []string `json:"methods"` +} + +// Do register the plugin, send info to reproxy conductor and activate RPC listener. +// On completion unregister from reproxy. Blocking call, should run in goroutine on the caller side +// rvcr is provided struct implemented at least one RPC methods with teh signature leike this: +// func(req lib.Request, res *lib.Response) (err error) +// see [examples/plugin]() for more info +func (p *Plugin) Do(ctx context.Context, conductor string, rcvr interface{}) (err error) { + + ctxCancel, cancel := context.WithCancel(ctx) + defer cancel() + + if err = rpc.RegisterName(p.Name, rcvr); err != nil { + return fmt.Errorf("can't register plugin %s: %v", p.Name, err) + } + log.Printf("[INFO] register rpc %s:%s", p.Name, p.Address) + + client := http.Client{Timeout: time.Second} + time.AfterFunc(time.Millisecond*50, func() { + // registration http call delayed to let listener time to start + err := repeater.NewDefault(10, time.Millisecond*500).Do(ctx, func() error { + return p.send(&client, conductor, "POST") + }) + if err != nil { + log.Printf("[ERROR] can't register with reproxy for %s: %v", p.Name, err) + cancel() + } + }) + + defer func() { + if e := p.send(&client, conductor, "DELETE"); e != nil { + log.Printf("[WARN] can't unregister with reproxy for %s: %v", p.Name, err) + } + }() + + return p.listen(ctxCancel) +} + +func (p *Plugin) listen(ctx context.Context) error { + listener, err := net.Listen("tcp", p.Address) + if err != nil { + return fmt.Errorf("can't listen on %s: %v", p.Address, err) + } + + for { + log.Printf("[DEBUG] plugin listener for %s:%s activated", p.Name, p.Address) + conn, err := listener.Accept() + if err != nil { + select { + case <-ctx.Done(): + return ctx.Err() + default: + return fmt.Errorf("accept failed for %s: %v", p.Name, err) + } + } + go rpc.ServeConn(conn) + } +} + +func (p *Plugin) send(client *http.Client, conductor string, method string) error { + + if conductor == "" { + return nil + } + + data, err := json.Marshal(p) + if err != nil { + return err + } + req, err := http.NewRequest(method, conductor, bytes.NewReader(data)) + if err != nil { + return err + } + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("invalid status %s", resp.Status) + } + return nil +} diff --git a/examples/plugin/vendor/github.com/umputun/reproxy/lib/rpc.go b/examples/plugin/vendor/github.com/umputun/reproxy/lib/rpc.go new file mode 100644 index 0000000..9fd2b8f --- /dev/null +++ b/examples/plugin/vendor/github.com/umputun/reproxy/lib/rpc.go @@ -0,0 +1,31 @@ +package lib + +import ( + "net/http" +) + +// Request sent to plugins +type Request struct { + URL string + RemoteAddr string + Host string + Header http.Header + Route string // final destination + Match struct { + Server string + Src string + Dst string + ProviderID string + PingURL string + MatchType string + AssetsLocation string + AssetsWebRoot string + } +} + +// Response from plugin's handler call +type Response struct { + StatusCode int + HeadersIn http.Header + HeadersOut http.Header +} diff --git a/examples/plugin/vendor/modules.txt b/examples/plugin/vendor/modules.txt new file mode 100644 index 0000000..a6b92a0 --- /dev/null +++ b/examples/plugin/vendor/modules.txt @@ -0,0 +1,9 @@ +# github.com/go-pkgz/lgr v0.10.4 +github.com/go-pkgz/lgr +# github.com/go-pkgz/repeater v1.1.3 +github.com/go-pkgz/repeater +github.com/go-pkgz/repeater/strategy +# github.com/umputun/reproxy v0.6.0 => ../../ +## explicit +github.com/umputun/reproxy/lib +# github.com/umputun/reproxy => ../../ diff --git a/go.mod b/go.mod index abf02a9..86117dd 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/go-pkgz/lgr v0.10.4 + github.com/go-pkgz/repeater v1.1.3 github.com/go-pkgz/rest v1.9.2 github.com/gorilla/handlers v1.5.1 github.com/prometheus/client_golang v1.10.0 diff --git a/go.sum b/go.sum index a1b6c41..86a0d47 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 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/repeater v1.1.3 h1:q6+JQF14ESSy28Dd7F+wRelY4F+41HJ0LEy/szNnMiE= +github.com/go-pkgz/repeater v1.1.3/go.mod h1:hVTavuO5x3Gxnu8zW7d6sQBfAneKV8X2FjU48kGfpKw= github.com/go-pkgz/rest v1.9.2 h1:RyBBRXBYY6eBgTW3UGYOyT4VQPDiBBFh/tesELWsryQ= github.com/go-pkgz/rest v1.9.2/go.mod h1:wZ/dGipZUaF9to0vIQl7PwDHgWQDB0jsrFg1xnAKLDw= github.com/go-pkgz/rest v1.9.3-0.20210514184429-77a1bddb51db h1:PoIO+kDPc0A6m5xlRao4No1P9Ew4hdyZ4UFnX9fbanc= diff --git a/lib/plugin.go b/lib/plugin.go new file mode 100644 index 0000000..84d5963 --- /dev/null +++ b/lib/plugin.go @@ -0,0 +1,112 @@ +package lib + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "net/rpc" + "time" + + log "github.com/go-pkgz/lgr" + "github.com/go-pkgz/repeater" +) + +// Plugin provides cancelable rpc server used to run custom plugins +type Plugin struct { + Name string `json:"name"` + Address string `json:"address"` + Methods []string `json:"methods"` +} + +// Do register the plugin, send info to reproxy conductor and activate RPC listener. +// On completion unregister from reproxy. Blocking call, should run in goroutine on the caller side +// rvcr is provided struct implemented at least one RPC methods with the signature like this: +// func(req lib.Request, res *lib.Response) (err error) +// see [examples/plugin]() for more info +func (p *Plugin) Do(ctx context.Context, conductor string, rcvr interface{}) (err error) { + + ctxCancel, cancel := context.WithCancel(ctx) + defer cancel() + + if err = rpc.RegisterName(p.Name, rcvr); err != nil { + return fmt.Errorf("can't register plugin %s: %v", p.Name, err) + } + log.Printf("[INFO] register rpc %s:%s", p.Name, p.Address) + + client := http.Client{Timeout: time.Second} + time.AfterFunc(time.Millisecond*50, func() { + // registration http call delayed to let listener time to start + err = repeater.NewDefault(10, time.Millisecond*500).Do(ctx, func() error { + return p.send(&client, conductor, "POST") + }) + if err != nil { + log.Printf("[ERROR] can't register with reproxy for %s: %v", p.Name, err) + cancel() + } + }) + + defer func() { + if e := p.send(&client, conductor, "DELETE"); e != nil { + log.Printf("[WARN] can't unregister with reproxy for %s: %v", p.Name, err) + } + }() + + return p.listen(ctxCancel) +} + +func (p *Plugin) listen(ctx context.Context) error { + listener, err := net.Listen("tcp", p.Address) + if err != nil { + return fmt.Errorf("can't listen on %s: %v", p.Address, err) + } + + go func() { + <-ctx.Done() + if err := listener.Close(); err != nil { + log.Printf("[WARN] can't lose plugin listener") + } + }() + + for { + log.Printf("[DEBUG] plugin listener for %s:%s activated", p.Name, p.Address) + conn, err := listener.Accept() + if err != nil { + select { + case <-ctx.Done(): + return ctx.Err() + default: + return fmt.Errorf("accept failed for %s: %v", p.Name, err) + } + } + go rpc.ServeConn(conn) + } +} + +func (p *Plugin) send(client *http.Client, conductor, method string) error { + + if conductor == "" { + return nil + } + + data, err := json.Marshal(p) + if err != nil { + return err + } + req, err := http.NewRequest(method, conductor, bytes.NewReader(data)) + if err != nil { + return err + } + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("invalid status %s", resp.Status) + } + return nil +} diff --git a/lib/plugin_test.go b/lib/plugin_test.go new file mode 100644 index 0000000..dbb09c6 --- /dev/null +++ b/lib/plugin_test.go @@ -0,0 +1,85 @@ +package lib + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "sync/atomic" + "testing" + "time" + + log "github.com/go-pkgz/lgr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPlugin_Do(t *testing.T) { + + var postCalls int32 + var deleteCalls int32 + tsConductor := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "POST": + atomic.AddInt32(&postCalls, 1) + case "DELETE": + atomic.AddInt32(&deleteCalls, 1) + default: + t.Fatalf("unexpected method %s", r.Method) + } + t.Logf("registration: %+v", r) + })) + defer tsConductor.Close() + + u, err := url.Parse(tsConductor.URL) + require.NoError(t, err) + p := Plugin{Name: "Test1", Address: "localhost:12345", Methods: []string{"H1", "H2"}} + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + err = p.Do(ctx, "http://"+u.Host, new(TestingHandler)) + assert.EqualError(t, err, "context deadline exceeded") + assert.Equal(t, int32(1), atomic.LoadInt32(&postCalls)) + assert.Equal(t, int32(1), atomic.LoadInt32(&deleteCalls)) +} + +func TestPlugin_DoFailed(t *testing.T) { + tsConductor := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + })) + defer tsConductor.Close() + + u, err := url.Parse(tsConductor.URL) + require.NoError(t, err) + p := Plugin{Name: "Test2", Address: "localhost:12345", Methods: []string{"H1", "H2"}} + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + err = p.Do(ctx, "http://"+u.Host, new(TestingHandler)) + assert.EqualError(t, err, "context canceled") +} + +// TestingHandler is an example of middleware handler altering headers and stastus +type TestingHandler struct{} + +// HeaderThing adds key:val header to the response +func (h *TestingHandler) H1(req Request, res *Response) (err error) { + log.Printf("req: %+v", req) + res.HeadersOut = http.Header{} + res.HeadersOut.Add("key", "val") + res.HeadersIn = http.Header{} + res.HeadersIn.Add("token", "something") + res.StatusCode = 200 // each handler has to set status code + return +} + +// ErrorThing returns status 500 on "/fail" url. This terminated processing chain on reproxy side immediately +func (h *TestingHandler) H2(req Request, res *Response) (err error) { + log.Printf("req: %+v", req) + if req.URL == "/fail" { + res.StatusCode = 500 + return + } + res.StatusCode = 200 + return +} diff --git a/lib/rpc.go b/lib/rpc.go new file mode 100644 index 0000000..9fd2b8f --- /dev/null +++ b/lib/rpc.go @@ -0,0 +1,31 @@ +package lib + +import ( + "net/http" +) + +// Request sent to plugins +type Request struct { + URL string + RemoteAddr string + Host string + Header http.Header + Route string // final destination + Match struct { + Server string + Src string + Dst string + ProviderID string + PingURL string + MatchType string + AssetsLocation string + AssetsWebRoot string + } +} + +// Response from plugin's handler call +type Response struct { + StatusCode int + HeadersIn http.Header + HeadersOut http.Header +} diff --git a/vendor/github.com/go-pkgz/repeater/.gitignore b/vendor/github.com/go-pkgz/repeater/.gitignore new file mode 100644 index 0000000..f1c181e --- /dev/null +++ b/vendor/github.com/go-pkgz/repeater/.gitignore @@ -0,0 +1,12 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out diff --git a/vendor/github.com/go-pkgz/repeater/.golangci.yml b/vendor/github.com/go-pkgz/repeater/.golangci.yml new file mode 100644 index 0000000..989af86 --- /dev/null +++ b/vendor/github.com/go-pkgz/repeater/.golangci.yml @@ -0,0 +1,60 @@ +linters-settings: + govet: + check-shadowing: true + golint: + min-confidence: 0 + gocyclo: + min-complexity: 15 + maligned: + suggest-new: true + dupl: + threshold: 100 + goconst: + min-len: 2 + min-occurrences: 2 + misspell: + locale: US + lll: + line-length: 140 + gocritic: + enabled-tags: + - performance + - style + - experimental + disabled-checks: + - wrapperFunc + +linters: + disable-all: true + enable: + - megacheck + - govet + - unconvert + - megacheck + - structcheck + - gas + - gocyclo + - dupl + - misspell + - unparam + - varcheck + - deadcode + - typecheck + - ineffassign + - varcheck + fast: false + + +run: +# modules-download-mode: vendor + skip-dirs: + - vendor + +issues: + exclude-rules: + - text: "weak cryptographic primitive" + linters: + - gosec + +service: + golangci-lint-version: 1.16.x \ No newline at end of file diff --git a/vendor/github.com/go-pkgz/repeater/.travis.yml b/vendor/github.com/go-pkgz/repeater/.travis.yml new file mode 100644 index 0000000..697ce97 --- /dev/null +++ b/vendor/github.com/go-pkgz/repeater/.travis.yml @@ -0,0 +1,20 @@ +language: go + +go: + - "1.12.x" + +install: true + +before_install: + - export TZ=America/Chicago + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.13.2 + - go get github.com/mattn/goveralls + - export PATH=$(pwd)/bin:$PATH + +script: + - GO111MODULE=on go get ./... + - GO111MODULE=on go mod vendor + - GO111MODULE=on go test -v -mod=vendor -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1; + - GO111MODULE=on go test -v -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1; + - golangci-lint run || travis_terminate 1; + - $GOPATH/bin/goveralls -coverprofile=profile.cov -service=travis-ci diff --git a/vendor/github.com/go-pkgz/repeater/LICENSE b/vendor/github.com/go-pkgz/repeater/LICENSE new file mode 100644 index 0000000..ac54025 --- /dev/null +++ b/vendor/github.com/go-pkgz/repeater/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Umputun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/go-pkgz/repeater/README.md b/vendor/github.com/go-pkgz/repeater/README.md new file mode 100644 index 0000000..76d8c90 --- /dev/null +++ b/vendor/github.com/go-pkgz/repeater/README.md @@ -0,0 +1,35 @@ +# Repeater [![Build Status](https://travis-ci.org/go-pkgz/repeater.svg?branch=master)](https://travis-ci.org/go-pkgz/repeater) [![Go Report Card](https://goreportcard.com/badge/github.com/go-pkgz/repeater)](https://goreportcard.com/report/github.com/go-pkgz/repeater) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/repeater/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/repeater?branch=master) + +Repeater calls a function until it returns no error, up to some number of iterations and delays defined by strategy. It terminates immediately on err from the provided (optional) list of critical errors. + +## Install and update + +`go get -u github.com/go-pkgz/repeater` + +## How to use + +New Repeater created by `New(strtg strategy.Interface)` or shortcut for defaults - `NewDefault(repeats int, delay time.Duration) *Repeater`. + +To activate invoke `Do` method. `Do` repeats func until no error returned. Predefined (optional) errors terminates the loop immediately. + +`func (r Repeater) Do(ctx context.Context, fun func() error, errors ...error) (err error)` + +### Repeating strategy + +User can provide his own strategy implementing the interface: + +```go +type Interface interface { + Start(ctx context.Context) chan struct{} +} +``` + +Returned channels used as "ticks," i.e., for each repeat or initial operation one read from this channel needed. Closing this channel indicates "done with retries." It is pretty much the same idea as `time.Timer` or `time.Tick` implements. Note - the first (technically not-repeated-yet) call won't happen **until something sent to the channel**. For this reason, the typical strategy sends the first "tick" before the first wait/sleep. + +Three most common strategies provided by package and ready to use: +1. **Fixed delay**, up to max number of attempts - `NewFixedDelay(repeats int, delay time.Duration)`. +It is the default strategy used by `repeater.NewDefault` constructor +2. **BackOff** with jitter provides exponential backoff. It starts from 100ms interval and goes in steps with `last * math.Pow(factor, attempt)`. Optional jitter randomizes intervals a little bit. The strategy created by `NewBackoff(repeats int, factor float64, jitter bool)`. _Factor = 1 effectively makes this strategy fixed with 100ms delay._ +3. **Once** strategy does not do any repeats and mainly used for tests/mocks - `NewOnce()` + + diff --git a/vendor/github.com/go-pkgz/repeater/go.mod b/vendor/github.com/go-pkgz/repeater/go.mod new file mode 100644 index 0000000..3488d7b --- /dev/null +++ b/vendor/github.com/go-pkgz/repeater/go.mod @@ -0,0 +1,5 @@ +module github.com/go-pkgz/repeater + +go 1.12 + +require github.com/stretchr/testify v1.3.0 diff --git a/vendor/github.com/go-pkgz/repeater/go.sum b/vendor/github.com/go-pkgz/repeater/go.sum new file mode 100644 index 0000000..4347755 --- /dev/null +++ b/vendor/github.com/go-pkgz/repeater/go.sum @@ -0,0 +1,7 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/vendor/github.com/go-pkgz/repeater/repeater.go b/vendor/github.com/go-pkgz/repeater/repeater.go new file mode 100644 index 0000000..e6370ce --- /dev/null +++ b/vendor/github.com/go-pkgz/repeater/repeater.go @@ -0,0 +1,64 @@ +// Package repeater call fun till it returns no error, up to repeat some number of iterations and delays defined by strategy. +// Repeats number and delays defined by strategy.Interface. Terminates immediately on err from +// provided, optional list of critical errors +package repeater + +import ( + "context" + "time" + + "github.com/go-pkgz/repeater/strategy" +) + +// Repeater is the main object, should be made by New or NewDefault, embeds strategy +type Repeater struct { + strategy.Interface +} + +// New repeater with a given strategy. If strategy=nil initializes with FixedDelay 5sec, 10 times. +func New(strtg strategy.Interface) *Repeater { + if strtg == nil { + strtg = &strategy.FixedDelay{Repeats: 10, Delay: time.Second * 5} + } + result := Repeater{Interface: strtg} + return &result +} + +// NewDefault makes repeater with FixedDelay strategy +func NewDefault(repeats int, delay time.Duration) *Repeater { + return New(&strategy.FixedDelay{Repeats: repeats, Delay: delay}) +} + +// Do repeats fun till no error. Predefined (optional) errors terminate immediately +func (r Repeater) Do(ctx context.Context, fun func() error, errors ...error) (err error) { + + ctx, cancelFunc := context.WithCancel(ctx) + defer cancelFunc() // ensure strategy's channel termination + + inErrors := func(err error) bool { + for _, e := range errors { + if e == err { + return true + } + } + return false + } + + ch := r.Start(ctx) // channel of ticks-like events provided by strategy + for { + select { + case <-ctx.Done(): + return ctx.Err() + case _, ok := <-ch: + if !ok { // closed channel indicates completion or early termination, set by strategy + return err + } + if err = fun(); err == nil { + return nil + } + if err != nil && inErrors(err) { // terminate on critical error from provided list + return err + } + } + } +} diff --git a/vendor/github.com/go-pkgz/repeater/strategy/backoff.go b/vendor/github.com/go-pkgz/repeater/strategy/backoff.go new file mode 100644 index 0000000..12f53b6 --- /dev/null +++ b/vendor/github.com/go-pkgz/repeater/strategy/backoff.go @@ -0,0 +1,59 @@ +package strategy + +import ( + "context" + "math" + "math/rand" + "sync" + "time" +) + +// Backoff implements strategy.Interface for exponential-backoff +// it starts from 100ms (by default, if no Duration set) and goes in steps with last * math.Pow(factor, attempt) +// optional jitter randomize intervals a little bit. +type Backoff struct { + Duration time.Duration + Repeats int + Factor float64 + Jitter bool + + once sync.Once +} + +// Start returns channel, similar to time.Timer +// then publishing signals to channel ch for retries attempt. Closed ch indicates "done" event +// consumer (repeater) should stop it explicitly after completion +func (b *Backoff) Start(ctx context.Context) <-chan struct{} { + + b.once.Do(func() { + if b.Duration == 0 { + b.Duration = 100 * time.Millisecond + } + if b.Repeats == 0 { + b.Repeats = 1 + } + if b.Factor <= 0 { + b.Factor = 1 + } + }) + + ch := make(chan struct{}) + go func() { + defer close(ch) + rnd := rand.New(rand.NewSource(int64(time.Now().Nanosecond()))) + for i := 0; i < b.Repeats; i++ { + select { + case <-ctx.Done(): + return + case ch <- struct{}{}: + } + + delay := float64(b.Duration) * math.Pow(b.Factor, float64(i)) + if b.Jitter { + delay = rnd.Float64()*(float64(2*b.Duration)) + (delay - float64(b.Duration)) + } + sleep(ctx, time.Duration(delay)) + } + }() + return ch +} diff --git a/vendor/github.com/go-pkgz/repeater/strategy/fixed.go b/vendor/github.com/go-pkgz/repeater/strategy/fixed.go new file mode 100644 index 0000000..ddd283e --- /dev/null +++ b/vendor/github.com/go-pkgz/repeater/strategy/fixed.go @@ -0,0 +1,36 @@ +package strategy + +import ( + "context" + "time" +) + +// FixedDelay implements strategy.Interface for fixed intervals up to max repeats +type FixedDelay struct { + Repeats int + Delay time.Duration +} + +// Start returns channel, similar to time.Timer +// then publishing signals to channel ch for retries attempt. +// can be terminated (canceled) via context. +func (s *FixedDelay) Start(ctx context.Context) <-chan struct{} { + if s.Repeats == 0 { + s.Repeats = 1 + } + ch := make(chan struct{}) + go func() { + defer func() { + close(ch) + }() + for i := 0; i < s.Repeats; i++ { + select { + case <-ctx.Done(): + return + case ch <- struct{}{}: + } + sleep(ctx, s.Delay) + } + }() + return ch +} diff --git a/vendor/github.com/go-pkgz/repeater/strategy/strategy.go b/vendor/github.com/go-pkgz/repeater/strategy/strategy.go new file mode 100644 index 0000000..75af12b --- /dev/null +++ b/vendor/github.com/go-pkgz/repeater/strategy/strategy.go @@ -0,0 +1,35 @@ +// Package strategy defines repeater's strategy and implements some. +// Strategy result is a channel acting like time.Timer ot time.Tick +package strategy + +import ( + "context" + "time" +) + +// Interface for repeater strategy. Returns channel with ticks +type Interface interface { + Start(ctx context.Context) <-chan struct{} +} + +// Once strategy eliminate repeats and makes a single try only +type Once struct{} + +// Start returns closed channel with a single element to prevent any repeats +func (s *Once) Start(ctx context.Context) <-chan struct{} { + ch := make(chan struct{}) + go func() { + ch <- struct{}{} + close(ch) + }() + return ch +} + +func sleep(ctx context.Context, duration time.Duration) { + select { + case <-time.After(duration): + return + case <-ctx.Done(): + return + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index bbdd242..f45f7ac 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -9,6 +9,10 @@ github.com/felixge/httpsnoop # github.com/go-pkgz/lgr v0.10.4 ## explicit github.com/go-pkgz/lgr +# github.com/go-pkgz/repeater v1.1.3 +## explicit +github.com/go-pkgz/repeater +github.com/go-pkgz/repeater/strategy # github.com/go-pkgz/rest v1.9.2 ## explicit github.com/go-pkgz/rest