1
0
mirror of https://github.com/umputun/reproxy.git synced 2024-11-16 20:25:52 +02:00

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
This commit is contained in:
Umputun 2021-06-01 02:56:39 -05:00 committed by GitHub
parent 30ca38b1ba
commit 7139c57766
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 4053 additions and 227 deletions

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
})
}
}

View File

@ -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
}

View File

@ -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,

View File

@ -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
}

85
app/plugin/client_mock.go Normal file
View File

@ -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
}

232
app/plugin/conductor.go Normal file
View File

@ -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()
}

View File

@ -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())
}

79
app/plugin/dialer_mock.go Normal file
View File

@ -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
}

117
app/proxy/handlers.go Normal file
View File

@ -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)
})
}
}

View File

@ -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)
}
}

189
app/proxy/matcher_mock.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -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"]

View File

@ -0,0 +1,2 @@
# Example of plugin

View File

@ -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"

7
examples/plugin/go.mod Normal file
View File

@ -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 => ../../

410
examples/plugin/go.sum Normal file
View File

@ -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=

50
examples/plugin/main.go Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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

21
examples/plugin/vendor/github.com/go-pkgz/lgr/LICENSE generated vendored Normal file
View File

@ -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.

124
examples/plugin/vendor/github.com/go-pkgz/lgr/README.md generated vendored Normal file
View File

@ -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()`

View File

@ -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)
}

5
examples/plugin/vendor/github.com/go-pkgz/lgr/go.mod generated vendored Normal file
View File

@ -0,0 +1,5 @@
module github.com/go-pkgz/lgr
require github.com/stretchr/testify v1.6.1
go 1.15

12
examples/plugin/vendor/github.com/go-pkgz/lgr/go.sum generated vendored Normal file
View File

@ -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=

View File

@ -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 }

407
examples/plugin/vendor/github.com/go-pkgz/lgr/logger.go generated vendored Normal file
View File

@ -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]
}

View File

@ -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 },
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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()`

View File

@ -0,0 +1,5 @@
module github.com/go-pkgz/repeater
go 1.12
require github.com/stretchr/testify v1.3.0

View File

@ -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=

View File

@ -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
}
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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.

View File

@ -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
}

View File

@ -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
}

9
examples/plugin/vendor/modules.txt vendored Normal file
View File

@ -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 => ../../

1
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

112
lib/plugin.go Normal file
View File

@ -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
}

85
lib/plugin_test.go Normal file
View File

@ -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
}

31
lib/rpc.go Normal file
View File

@ -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
}

12
vendor/github.com/go-pkgz/repeater/.gitignore generated vendored Normal file
View File

@ -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

60
vendor/github.com/go-pkgz/repeater/.golangci.yml generated vendored Normal file
View File

@ -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

20
vendor/github.com/go-pkgz/repeater/.travis.yml generated vendored Normal file
View File

@ -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

21
vendor/github.com/go-pkgz/repeater/LICENSE generated vendored Normal file
View File

@ -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.

35
vendor/github.com/go-pkgz/repeater/README.md generated vendored Normal file
View File

@ -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()`

5
vendor/github.com/go-pkgz/repeater/go.mod generated vendored Normal file
View File

@ -0,0 +1,5 @@
module github.com/go-pkgz/repeater
go 1.12
require github.com/stretchr/testify v1.3.0

7
vendor/github.com/go-pkgz/repeater/go.sum generated vendored Normal file
View File

@ -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=

64
vendor/github.com/go-pkgz/repeater/repeater.go generated vendored Normal file
View File

@ -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
}
}
}
}

59
vendor/github.com/go-pkgz/repeater/strategy/backoff.go generated vendored Normal file
View File

@ -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
}

36
vendor/github.com/go-pkgz/repeater/strategy/fixed.go generated vendored Normal file
View File

@ -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
}

View File

@ -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
}
}

4
vendor/modules.txt vendored
View File

@ -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