diff options
author | Valery Piashchynski <[email protected]> | 2020-12-26 00:40:31 +0300 |
---|---|---|
committer | Valery Piashchynski <[email protected]> | 2020-12-26 00:40:31 +0300 |
commit | 7a0dee1a416705c621edbf50e1f43fb39845348f (patch) | |
tree | 0007a6b8c8ac9e7d31b8a5f3f7f27669c860d261 | |
parent | 8526c03822e724bc2ebb64b6197085fea335b782 (diff) |
Huge tests refactoring. Reduce running time 2-3x times
122 files changed, 7833 insertions, 386 deletions
@@ -32,7 +32,18 @@ test: ## Run application tests go test -v -race -cover -tags=debug -covermode=atomic ./pkg/worker go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/http go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/informer + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/reload go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/server + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/checker + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/config + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/gzip + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/headers + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/logger + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/metrics + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/redis + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/resetter + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/rpc + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/static lint: ## Run application linters go fmt ./... diff --git a/cmd/cli/root.go b/cmd/cli/root.go index febe410b..eb15f565 100644 --- a/cmd/cli/root.go +++ b/cmd/cli/root.go @@ -8,9 +8,9 @@ import ( "github.com/spiral/errors" goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" - rpcPlugin "github.com/spiral/roadrunner-plugins/rpc" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" - "github.com/spiral/roadrunner-plugins/config" + "github.com/spiral/roadrunner/v2/plugins/config" "github.com/spf13/cobra" "github.com/spiral/endure" diff --git a/cmd/cli/workers.go b/cmd/cli/workers.go index 94c339e2..03639aa4 100644 --- a/cmd/cli/workers.go +++ b/cmd/cli/workers.go @@ -13,7 +13,7 @@ import ( "github.com/fatih/color" "github.com/spf13/cobra" "github.com/spiral/errors" - "github.com/spiral/roadrunner/v2/pkg/plugins/informer" + "github.com/spiral/roadrunner/v2/plugins/informer" "github.com/spiral/roadrunner/v2/tools" ) diff --git a/cmd/main.go b/cmd/main.go index ec3d84ff..4e7fc099 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,17 +4,17 @@ import ( "log" "github.com/spiral/endure" - "github.com/spiral/roadrunner/v2/pkg/plugins/http" - "github.com/spiral/roadrunner/v2/pkg/plugins/informer" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" + "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/informer" + "github.com/spiral/roadrunner/v2/plugins/server" - "github.com/spiral/roadrunner-plugins/logger" - "github.com/spiral/roadrunner-plugins/metrics" - "github.com/spiral/roadrunner-plugins/redis" - "github.com/spiral/roadrunner-plugins/reload" - "github.com/spiral/roadrunner-plugins/resetter" - "github.com/spiral/roadrunner-plugins/rpc" "github.com/spiral/roadrunner/v2/cmd/cli" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/metrics" + "github.com/spiral/roadrunner/v2/plugins/redis" + "github.com/spiral/roadrunner/v2/plugins/reload" + "github.com/spiral/roadrunner/v2/plugins/resetter" + "github.com/spiral/roadrunner/v2/plugins/rpc" ) func main() { @@ -3,32 +3,33 @@ module github.com/spiral/roadrunner/v2 go 1.15 require ( + github.com/NYTimes/gziphandler v1.1.1 + github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect + github.com/alicebob/miniredis/v2 v2.14.1 github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 github.com/fatih/color v1.10.0 + github.com/go-ole/go-ole v1.2.4 // indirect + github.com/go-redis/redis/v8 v8.4.4 + github.com/gofiber/fiber/v2 v2.3.0 github.com/golang/mock v1.4.4 github.com/hashicorp/go-multierror v1.1.0 github.com/json-iterator/go v1.1.10 github.com/mattn/go-runewidth v0.0.9 github.com/olekukonko/tablewriter v0.0.4 + github.com/prometheus/client_golang v0.9.3 github.com/shirou/gopsutil v3.20.11+incompatible github.com/spf13/cobra v1.1.1 + github.com/spf13/viper v1.7.0 github.com/spiral/endure v1.0.0-beta20 github.com/spiral/errors v1.0.6 github.com/spiral/goridge/v3 v3.0.0-beta8 - github.com/spiral/roadrunner-plugins/checker v1.0.1 - github.com/spiral/roadrunner-plugins/config v1.0.1 - github.com/spiral/roadrunner-plugins/logger v1.0.2 - github.com/spiral/roadrunner-plugins/metrics v1.0.1 - github.com/spiral/roadrunner-plugins/redis v1.0.1 - github.com/spiral/roadrunner-plugins/reload v1.0.1 - github.com/spiral/roadrunner-plugins/resetter v1.0.1 - github.com/spiral/roadrunner-plugins/rpc v1.0.2 github.com/stretchr/testify v1.6.1 github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a github.com/vbauerster/mpb/v5 v5.4.0 github.com/yookoala/gofast v0.4.0 go.uber.org/multierr v1.6.0 + go.uber.org/zap v1.16.0 golang.org/x/net v0.0.0-20201216054612-986b41b23924 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a golang.org/x/sys v0.0.0-20201221093633-bc327ba9c2f0 @@ -14,52 +14,35 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -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/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -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/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis/v2 v2.14.1 h1:GjlbSeoJ24bzdLRs13HoMEeaRZx9kg5nHoRW7QV/nCs= github.com/alicebob/miniredis/v2 v2.14.1/go.mod h1:uS970Sw5Gs9/iK3yBg0l9Uj9s25wXxSpQUE9EaJ/Blg= github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -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 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 h1:gfAMKE626QEuKG3si0pdTRcr/YEbBoxY+3GOH3gWvl4= github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U= -github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= -github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc= github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= @@ -67,21 +50,13 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -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/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.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/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/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/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -91,19 +66,10 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs= 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 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -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 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= @@ -112,28 +78,20 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-ini/ini v1.38.1/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 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-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-redis/redis/v8 v8.4.4 h1:fGqgxCTR1sydaKI00oQf3OmkU/DIe/I/fYXvGklCIuc= github.com/go-redis/redis/v8 v8.4.4/go.mod h1:nA0bQuF0i5JFx4Ta9RZxGKXFrQ8cRWntra97f0196iY= github.com/go-restit/lzjson v0.0.0-20161206095556-efe3c53acc68/go.mod h1:7vXSKQt83WmbPeyVjCfNT9YDJ5BUFmcwFsEjI9SCvYM= -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/gofiber/fiber/v2 v2.3.0 h1:82ufvLne0cxzdkDOeLkUmteA+z1uve9JQ/ZFsMOnkzc= github.com/gofiber/fiber/v2 v2.3.0/go.mod h1:f8BRRIMjMdRyt2qmJ/0Sea3j3rwwfufPrh9WNBRiVZ0= -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-20190129154638-5b532d6fd5ef/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/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= @@ -149,10 +107,8 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= -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= @@ -166,26 +122,17 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -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.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -200,7 +147,6 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX 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= @@ -211,16 +157,10 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m 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 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -228,23 +168,18 @@ github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVY github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.10.7 h1:7rix8v8GpI3ZBb0nSozFRgbtXKv+hOe+qfEpZqybrAg= github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= -github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= @@ -253,10 +188,8 @@ github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -279,107 +212,51 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/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/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= 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/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U= github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= -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/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -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 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -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.9.0 h1:Rrch9mh17XcxvEu9D9DEpb4isxjGBtcevQjKvxPRQIU= -github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU= 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 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM= -github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= -github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -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/shirou/gopsutil v3.20.11+incompatible h1:LJr4ZQK4mPpIV5gOa4jCOKOGb4ty4DZO54I4FGqIpto= github.com/shirou/gopsutil v3.20.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= 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-20180820201707-7c9eb446e3cf/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -387,25 +264,21 @@ github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:X github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= -github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spiral/endure v1.0.0-beta20 h1:QD3EJ6CRLgeo/6trfnlUcQhH3vrK8Hvf9ceDpde+yss= github.com/spiral/endure v1.0.0-beta20/go.mod h1:qCU2/4gAItVESzUK0yPExmUTlTcpRLqJUgcV+nqxn+o= github.com/spiral/errors v1.0.4/go.mod h1:SwMSZVdZkkJVgXNNafccqOaxWg0XPzVU/dEdUEInE0o= @@ -415,72 +288,6 @@ github.com/spiral/errors v1.0.6 h1:berk5ShEILSw6DplUVv9Ea1wGdk2WlVKQpuvDngll0U= github.com/spiral/errors v1.0.6/go.mod h1:SwMSZVdZkkJVgXNNafccqOaxWg0XPzVU/dEdUEInE0o= github.com/spiral/goridge/v3 v3.0.0-beta8 h1:x8uXCdhY49U1LEvmehnTaD2El6J9ZHAefRdh/QIZ6A4= github.com/spiral/goridge/v3 v3.0.0-beta8/go.mod h1:XFQGc42KNzo/hPIXPki7mEkFTf9v/T7qFk/TYJjMtzE= -github.com/spiral/roadrunner-plugins v0.0.0-20201224162001-79cb05174578 h1:zFQuyy/RMMVsQtD6y2RAPO4eBsWwGvsB04BuEmXOVD4= -github.com/spiral/roadrunner-plugins/a_plugin_tests v0.0.0-20201224162001-79cb05174578 h1:E4cenNwBMi0F9327C7WMKx484MwFs5etbRcYKs1+EgY= -github.com/spiral/roadrunner-plugins/a_plugin_tests v0.0.0-20201224162001-79cb05174578/go.mod h1:0aZijFwgc35Px78nXqOZNjJjd1jaHGxejGJrd8dJnuc= -github.com/spiral/roadrunner-plugins/checker v1.0.1 h1:UVMX9zUF0R3ScRFUQg89e5+CLn3I2e5uAKsD4A52iDU= -github.com/spiral/roadrunner-plugins/checker v1.0.1/go.mod h1:/gnVybeKye5uq/8hgjI7YDoK/ntPd0ej6tWkxlYdiz8= -github.com/spiral/roadrunner-plugins/checker v1.0.3 h1:fnWX2PBXlNhYp8UERgFx/eMM1Qf4pDJCeUY4NoiWwbo= -github.com/spiral/roadrunner-plugins/checker v1.0.3/go.mod h1:ZLMg9NJsqUuCG7akU2V6JAFd/9ZLjawzvoJRYZxr5b0= -github.com/spiral/roadrunner-plugins/config v1.0.0 h1:RPYB8Ha/UeuBGRwtcqNb0uU8R5qe4XpUXVmv/lGSUdQ= -github.com/spiral/roadrunner-plugins/config v1.0.0/go.mod h1:XcVJLFDUlYPvZ3kVzssmP4fJbEzUvVJf534+eZaotAo= -github.com/spiral/roadrunner-plugins/config v1.0.1 h1:6n9mFqvy+Fk3LaOWfNSA7lb5o2vJOnGJivNFP7Fg8QA= -github.com/spiral/roadrunner-plugins/config v1.0.1/go.mod h1:MQGXiAP8Sr6/LjFVcr+AeUElNMKJXnwVEKXn1I5ztXk= -github.com/spiral/roadrunner-plugins/gzip v1.0.0/go.mod h1:gQSyvp7u8udOwZac5o2vcfrGANIaozyrIU1Q+xXq328= -github.com/spiral/roadrunner-plugins/headers v1.0.0/go.mod h1:bZAGI0wo465MlYNx0Urpss0GyQDJQY5tcY9YrgiUElE= -github.com/spiral/roadrunner-plugins/http v1.0.0 h1:8WGAuZOrkYZQWo6n13ip+ZtzhKugZZ+b5W+ivVr7FjI= -github.com/spiral/roadrunner-plugins/http v1.0.0/go.mod h1:37ReUuAKJDtXH3GjMjRH5q3plBXq5r5lUfltRTVZzDE= -github.com/spiral/roadrunner-plugins/http v1.0.2 h1:BRmdbx0DYlgYTkvmd1IDZJewC1y30GqBCKRjbTn3OLc= -github.com/spiral/roadrunner-plugins/http v1.0.2/go.mod h1:JRgVSgJRh6wDmVs1rFJHQ9PM0xtCpzX9vBtVDItXZ/E= -github.com/spiral/roadrunner-plugins/http v1.0.5 h1:6uHbc2OwhExBE9Psfwc2WDn6KeIXzLjiCCwO9yBH9JU= -github.com/spiral/roadrunner-plugins/http v1.0.5/go.mod h1:9ZlPZJ1A7M3aBQ/tJYjmuXYPox3smFP8ccsf+EBHd9c= -github.com/spiral/roadrunner-plugins/informer v1.0.3 h1:6DXs8IRDjBvAr2mGKXVlhx1KLLfVqpXgvP2va79kR1s= -github.com/spiral/roadrunner-plugins/informer v1.0.3/go.mod h1:OPEJNADBbNQyx0/KuXQbY3Mqemo30vZh6duf6YpRf7M= -github.com/spiral/roadrunner-plugins/informer v1.0.4 h1:J2DXrQkRRyYDGw/6TIo7qF74Zn3AOHQe+9g0Hj5BwR4= -github.com/spiral/roadrunner-plugins/informer v1.0.4/go.mod h1:4qvFbVw0ESwE5RJQRiP939QN3SJ8l6aDnrQhL09f5lQ= -github.com/spiral/roadrunner-plugins/informer v1.0.5 h1:L27oLcHONKL3L4X3uR/6ZSZF9iz27urT+uIsWGOM4e4= -github.com/spiral/roadrunner-plugins/informer v1.0.5/go.mod h1:BcJewp6L0cEvX8uaYwemsWa7roVCLjQGEsAeBsRvmN0= -github.com/spiral/roadrunner-plugins/logger v1.0.1 h1:cc/pRE+8bUQgLiF05G7f948UpWtrZmJk/axTh1ywrks= -github.com/spiral/roadrunner-plugins/logger v1.0.1/go.mod h1:/oX1P56+/joLaAPwZaiRqPVVzM36701kdYqOHu+U3EM= -github.com/spiral/roadrunner-plugins/logger v1.0.2 h1:4dBDKMUYdwE3jQ6ld1cR7E+ApRE30cDFudHSwDpgmx8= -github.com/spiral/roadrunner-plugins/logger v1.0.2/go.mod h1:TGafnqSIQak7e+7mkrdatl379VVlK0kZE874ZC5exPk= -github.com/spiral/roadrunner-plugins/metrics v1.0.0 h1:PEK+AlcMv6yrH/vag7yQm/5sV7gMoWsNn9KWOsDPw/0= -github.com/spiral/roadrunner-plugins/metrics v1.0.0/go.mod h1:zLd9TdARJSwo6tQjTb0auD7pxN2ODXnsy4evInCMp9o= -github.com/spiral/roadrunner-plugins/metrics v1.0.1 h1:Ifbioo0BgsMk83sQfv36hpDwqC5iL4SFaEsLKeTRdXs= -github.com/spiral/roadrunner-plugins/metrics v1.0.1/go.mod h1:1Ixk+pmFcZQEigB/km6ABkCwhTl4ph3YWDZMN/g4YHI= -github.com/spiral/roadrunner-plugins/redis v1.0.0 h1:/J4JpbNLd0G23BD9VaHsXKR8OHyAnrRQf7rRZMFx57U= -github.com/spiral/roadrunner-plugins/redis v1.0.0/go.mod h1:VzXRQcNaNONaUxyCW9t9eC+p8UxkXx2kwGeVw3s7zR0= -github.com/spiral/roadrunner-plugins/redis v1.0.1 h1:RGVbCsADfoEXnIeIJvBRsxoMxVh7Gx5p/YrVnYPTyd8= -github.com/spiral/roadrunner-plugins/redis v1.0.1/go.mod h1:r9FiOvBzwiWEWNMuQf45j7c8X01nrR3bpS1BxZl0bfY= -github.com/spiral/roadrunner-plugins/reload v1.0.0 h1:cBevNueyJjG4ykTjd9C4WSV8xNJq3UuXh3ympp3GU0I= -github.com/spiral/roadrunner-plugins/reload v1.0.0/go.mod h1:IEDqSrV0zbnvCNXigLtp0EoGVsHc34merlU2gokiuDs= -github.com/spiral/roadrunner-plugins/reload v1.0.1 h1:K7uvITE2B9bZyYWXk2b82erCeWjFBdjcCdfcXDVBjlU= -github.com/spiral/roadrunner-plugins/reload v1.0.1/go.mod h1:IEDqSrV0zbnvCNXigLtp0EoGVsHc34merlU2gokiuDs= -github.com/spiral/roadrunner-plugins/resetter v1.0.0 h1:Nx4mIzeoH/IcUfY4LM9xhY0yAXXYkAIAqWCRdMcsr68= -github.com/spiral/roadrunner-plugins/resetter v1.0.0/go.mod h1:DLFifJk1n3PWViXkT5+qAmzeRcPTowDRSbRqotf+WlE= -github.com/spiral/roadrunner-plugins/resetter v1.0.1 h1:U9nrxhz8graviJ8LjClFCA3+H346Obhr/xsoCwTlAsQ= -github.com/spiral/roadrunner-plugins/resetter v1.0.1/go.mod h1:vz7RFxWu0P79OGoGN0w2x63RIqszZCCGjEvMwtHHjDE= -github.com/spiral/roadrunner-plugins/rpc v1.0.0 h1:cC17yCNqQUNtedKefdeT2P6z7q52L0QakxS4qwB7n+g= -github.com/spiral/roadrunner-plugins/rpc v1.0.0/go.mod h1:p+ClRf1ibW+xvekf+nGQtvipyrtPJP6WZ/J/DSp+Qck= -github.com/spiral/roadrunner-plugins/rpc v1.0.1 h1:rbMJNM+4opzoB7ZXi9dbntfix9+cTuQdUMcDIWsaC7k= -github.com/spiral/roadrunner-plugins/rpc v1.0.1/go.mod h1:iz1a3poEKCppX1fYfneMWOxWBfToYSi2TE1rYD0yBls= -github.com/spiral/roadrunner-plugins/rpc v1.0.2 h1:1ICsIasYP8W6cEAMEzU6H+VqGh3AFaCWyy4w8VfRYg4= -github.com/spiral/roadrunner-plugins/rpc v1.0.2/go.mod h1:DAioNb54q5WIOOzdo+2IJB9IysM2WI7f9nzQcNBp2Xk= -github.com/spiral/roadrunner-plugins/server v1.0.1 h1:yzCw5rgz+KHpJb56QIID3gWH/IBbNtHZD2zvkck1bYc= -github.com/spiral/roadrunner-plugins/server v1.0.1/go.mod h1:qedfnQFlK1+Jwv5M8mRXJCOWTvF/qfFyULK/0UMBeOk= -github.com/spiral/roadrunner-plugins/server v1.0.3 h1:Bhr5tK0eW+52/qeew6QMowowOpeUKJv/qqECCmW1X+Y= -github.com/spiral/roadrunner-plugins/server v1.0.3/go.mod h1:XHfOZwvITuKyUwU79KaeMm8gU5AqzKQg0MCHJ7eBU5c= -github.com/spiral/roadrunner-plugins/server v1.0.4 h1:i6QWbVM/gLnM3Tb/DDFSFpRnxccJd3mCpENW2rBuTeE= -github.com/spiral/roadrunner-plugins/server v1.0.4/go.mod h1:XHfOZwvITuKyUwU79KaeMm8gU5AqzKQg0MCHJ7eBU5c= -github.com/spiral/roadrunner-plugins/server v1.0.6 h1:JOrA/O/DJjuqKE1YTxLSyYh+/FLsJHY9biZkLwZk+LE= -github.com/spiral/roadrunner-plugins/server v1.0.6/go.mod h1:FyUujaAfNNdXCJHDkWD5pcRolNQ4cOMb6N+KapIMVpk= -github.com/spiral/roadrunner-plugins/static v1.0.0/go.mod h1:lipuYhoSoZR95DR6hO7MOdaJKQq4JibCPC2V2BDzNXg= -github.com/spiral/roadrunner/v2 v2.0.0-alpha26/go.mod h1:r7ojuHm9qCVbg4fKcqr4Aqk7VXqZ9YPefr1LOv7HNys= -github.com/spiral/roadrunner/v2 v2.0.0-alpha28/go.mod h1:Uqtk/47S+qweVjSl4R9XVImEVZc3nMWku4yg3A+f18Q= -github.com/spiral/roadrunner/v2 v2.0.0-alpha30/go.mod h1:fKmPelVQxWql3cpjt/hYsf/Y2zDKb+DbZXgxdwj2nTc= -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= @@ -490,10 +297,7 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.18.0 h1:IV0DdMlatq9QO1Cr6wGJPVW1sV1Q8HvZXAIcjorylyM= @@ -507,31 +311,23 @@ github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6Ac github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yookoala/gofast v0.4.0 h1:dLBjghcsbbZNOEHN8N1X/gh9S6srmJed4WQfG7DlKwo= github.com/yookoala/gofast v0.4.0/go.mod h1:rfbkoKaQG1bnuTUZcmV3vAlnfpF4FTq8WbQJf2vcpg8= +github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0= github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -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.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/otel v0.15.0 h1:CZFy2lPhxd4HlhZnYK8gRyDotksO3Ip9rBweY1vVYJw= go.opentelemetry.io/otel v0.15.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/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= go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -540,8 +336,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -562,7 +356,6 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -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= @@ -571,19 +364,15 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r 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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/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 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY= @@ -597,7 +386,6 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= 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= @@ -607,35 +395,26 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h 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-20190204203706-41f3e6584952/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/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-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201210223839-7e3030f88018/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 h1:+CBz4km/0KPU3RGTwARGh/noP3bEwtHcq+0YcBQM2JQ= golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201221093633-bc327ba9c2f0 h1:n+DPcgTwkgWzIFpLmoimYR2K2b0Ga5+Os4kayIN0vGo= @@ -647,13 +426,10 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/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-20180726210403-bfb5194568d3/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= @@ -663,7 +439,6 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -674,20 +449,15 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114 h1:DnSr2mCsxyCE6ZgIkmcWUQY2R5cH/6wL7eIxEmQOMSE= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 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/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= @@ -697,21 +467,13 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/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-20190502173448-54afdca5d873/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-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -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.21.1/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= @@ -723,37 +485,28 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/ini.v1 v1.38.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 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 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/plugins/checker/config.go b/plugins/checker/config.go new file mode 100644 index 00000000..5f952592 --- /dev/null +++ b/plugins/checker/config.go @@ -0,0 +1,5 @@ +package checker + +type Config struct { + Address string +} diff --git a/plugins/checker/interface.go b/plugins/checker/interface.go new file mode 100644 index 00000000..dd9dcada --- /dev/null +++ b/plugins/checker/interface.go @@ -0,0 +1,11 @@ +package checker + +// Status consists of status code from the service +type Status struct { + Code int +} + +// Checker interface used to get latest status from plugin +type Checker interface { + Status() Status +} diff --git a/plugins/checker/plugin.go b/plugins/checker/plugin.go new file mode 100644 index 00000000..95f4f68c --- /dev/null +++ b/plugins/checker/plugin.go @@ -0,0 +1,151 @@ +package checker + +import ( + "fmt" + "net/http" + "time" + + "github.com/gofiber/fiber/v2" + fiberLogger "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +const ( + // PluginName declares public plugin name. + PluginName = "status" +) + +type Plugin struct { + registry map[string]Checker + server *fiber.App + log logger.Logger + cfg *Config +} + +func (c *Plugin) Init(log logger.Logger, cfg config.Configurer) error { + const op = errors.Op("status plugin init") + err := cfg.UnmarshalKey(PluginName, &c.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + if c.cfg == nil { + return errors.E(errors.Disabled) + } + + c.registry = make(map[string]Checker) + c.log = log + return nil +} + +func (c *Plugin) Serve() chan error { + errCh := make(chan error, 1) + c.server = fiber.New(fiber.Config{ + ReadTimeout: time.Second * 5, + WriteTimeout: time.Second * 5, + IdleTimeout: time.Second * 5, + }) + c.server.Group("/v1", c.healthHandler) + c.server.Use(fiberLogger.New()) + c.server.Use("/health", c.healthHandler) + + go func() { + err := c.server.Listen(c.cfg.Address) + if err != nil { + errCh <- err + } + }() + + return errCh +} + +func (c *Plugin) Stop() error { + const op = errors.Op("checker stop") + err := c.server.Shutdown() + if err != nil { + return errors.E(op, err) + } + return nil +} + +// Reset named service. +func (c *Plugin) Status(name string) (Status, error) { + const op = errors.Op("get status") + svc, ok := c.registry[name] + if !ok { + return Status{}, errors.E(op, errors.Errorf("no such service: %s", name)) + } + + return svc.Status(), nil +} + +// CollectTarget collecting services which can provide Status. +func (c *Plugin) CollectTarget(name endure.Named, r Checker) error { + c.registry[name.Name()] = r + return nil +} + +// Collects declares services to be collected. +func (c *Plugin) Collects() []interface{} { + return []interface{}{ + c.CollectTarget, + } +} + +// Name of the service. +func (c *Plugin) Name() string { + return PluginName +} + +// RPCService returns associated rpc service. +func (c *Plugin) RPC() interface{} { + return &rpc{srv: c, log: c.log} +} + +type Plugins struct { + Plugins []string `query:"plugin"` +} + +const template string = "Service: %s: Status: %d\n" + +func (c *Plugin) healthHandler(ctx *fiber.Ctx) error { + const op = errors.Op("health_handler") + plugins := &Plugins{} + err := ctx.QueryParser(plugins) + if err != nil { + return errors.E(op, err) + } + + if len(plugins.Plugins) == 0 { + ctx.Status(http.StatusOK) + _, _ = ctx.WriteString("No plugins provided in query. Query should be in form of: /v1/health?plugin=plugin1&plugin=plugin2 \n") + return nil + } + + failed := false + // iterate over all provided plugins + for i := 0; i < len(plugins.Plugins); i++ { + // check if the plugin exists + if plugin, ok := c.registry[plugins.Plugins[i]]; ok { + st := plugin.Status() + if st.Code >= 500 { + failed = true + continue + } else if st.Code >= 100 && st.Code <= 400 { + _, _ = ctx.WriteString(fmt.Sprintf(template, plugins.Plugins[i], st.Code)) + } + } else { + _, _ = ctx.WriteString(fmt.Sprintf("Service: %s not found", plugins.Plugins[i])) + } + } + if failed { + ctx.Status(http.StatusInternalServerError) + return nil + } + + ctx.Status(http.StatusOK) + return nil +} diff --git a/plugins/checker/rpc.go b/plugins/checker/rpc.go new file mode 100644 index 00000000..0daa62fe --- /dev/null +++ b/plugins/checker/rpc.go @@ -0,0 +1,27 @@ +package checker + +import ( + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +type rpc struct { + srv *Plugin + log logger.Logger +} + +// Status return current status of the provided plugin +func (rpc *rpc) Status(service string, status *Status) error { + const op = errors.Op("status") + rpc.log.Debug("started Status method", "service", service) + st, err := rpc.srv.Status(service) + if err != nil { + return errors.E(op, err) + } + + *status = st + + rpc.log.Debug("status code", "code", st.Code) + rpc.log.Debug("successfully finished Status method") + return nil +} diff --git a/plugins/config/interface.go b/plugins/config/interface.go new file mode 100644 index 00000000..2a7c67ce --- /dev/null +++ b/plugins/config/interface.go @@ -0,0 +1,22 @@ +package config + +type Configurer interface { + // UnmarshalKey reads configuration section into configuration object. + // + // func (h *HttpService) Init(cp config.Configurer) error { + // h.config := &HttpConfig{} + // if err := configProvider.UnmarshalKey("http", h.config); err != nil { + // return err + // } + // } + UnmarshalKey(name string, out interface{}) error + + // Get used to get config section + Get(name string) interface{} + + // Overwrite used to overwrite particular values in the unmarshalled config + Overwrite(values map[string]interface{}) error + + // Has checks if config section exists. + Has(name string) bool +} diff --git a/plugins/config/plugin.go b/plugins/config/plugin.go new file mode 100755 index 00000000..1a170448 --- /dev/null +++ b/plugins/config/plugin.go @@ -0,0 +1,75 @@ +package config + +import ( + "bytes" + "strings" + + "github.com/spf13/viper" + "github.com/spiral/errors" +) + +type Viper struct { + viper *viper.Viper + Path string + Prefix string + Type string + ReadInCfg []byte +} + +// Inits config provider. +func (v *Viper) Init() error { + const op = errors.Op("viper plugin init") + v.viper = viper.New() + // If user provided []byte data with config, read it and ignore Path and Prefix + if v.ReadInCfg != nil && v.Type != "" { + v.viper.SetConfigType("yaml") + return v.viper.ReadConfig(bytes.NewBuffer(v.ReadInCfg)) + } + + // read in environment variables that match + v.viper.AutomaticEnv() + if v.Prefix == "" { + return errors.E(op, errors.Str("prefix should be set")) + } + + v.viper.SetEnvPrefix(v.Prefix) + if v.Path == "" { + return errors.E(op, errors.Str("path should be set")) + } + + v.viper.SetConfigFile(v.Path) + v.viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + return v.viper.ReadInConfig() +} + +// Overwrite overwrites existing config with provided values +func (v *Viper) Overwrite(values map[string]interface{}) error { + if len(values) != 0 { + for key, value := range values { + v.viper.Set(key, value) + } + } + + return nil +} + +// UnmarshalKey reads configuration section into configuration object. +func (v *Viper) UnmarshalKey(name string, out interface{}) error { + const op = errors.Op("unmarshal key") + err := v.viper.UnmarshalKey(name, &out) + if err != nil { + return errors.E(op, err) + } + return nil +} + +// Get raw config in a form of config section. +func (v *Viper) Get(name string) interface{} { + return v.viper.Get(name) +} + +// Has checks if config section exists. +func (v *Viper) Has(name string) bool { + return v.viper.IsSet(name) +} diff --git a/plugins/doc/graphviz.svg b/plugins/doc/graphviz.svg new file mode 100644 index 00000000..86f6ab5c --- /dev/null +++ b/plugins/doc/graphviz.svg @@ -0,0 +1,169 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><!-- Generated by graphviz version 2.40.1 (20161225.0304) + --><!-- Title: endure Pages: 1 --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="625pt" height="479pt" viewBox="0.00 0.00 624.94 478.79"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 474.786)"> +<title>endure</title> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-474.786 620.9357,-474.786 620.9357,4 -4,4"/> +<!-- checker --> +<g id="node1" class="node"> +<title>checker</title> +<ellipse fill="none" stroke="#000000" cx="412.2429" cy="-377.2862" rx="41.1103" ry="18"/> +<text text-anchor="middle" x="412.2429" y="-373.0862" font-family="Times,serif" font-size="14.00" fill="#000000">checker</text> +</g> +<!-- config --> +<g id="node2" class="node"> +<title>config</title> +<ellipse fill="none" stroke="#000000" cx="463.8878" cy="-235.393" rx="35.9154" ry="18"/> +<text text-anchor="middle" x="463.8878" y="-231.193" font-family="Times,serif" font-size="14.00" fill="#000000">config</text> +</g> +<!-- checker->config --> +<g id="edge1" class="edge"> +<title>checker->config</title> +<path fill="none" stroke="#000000" d="M418.7837,-359.3154C427.6313,-335.0068 443.4953,-291.4209 453.8554,-262.9568"/> +<polygon fill="#000000" stroke="#000000" points="457.2687,-263.812 457.4,-253.218 450.6908,-261.4178 457.2687,-263.812"/> +</g> +<!-- logger --> +<g id="node3" class="node"> +<title>logger</title> +<ellipse fill="none" stroke="#000000" cx="35.7071" cy="-310.8928" rx="35.9154" ry="18"/> +<text text-anchor="middle" x="35.7071" y="-306.6928" font-family="Times,serif" font-size="14.00" fill="#000000">logger</text> +</g> +<!-- checker->logger --> +<g id="edge2" class="edge"> +<title>checker->logger</title> +<path fill="none" stroke="#000000" d="M374.0665,-370.5547C303.7112,-358.1492 154.0014,-331.7513 79.586,-318.6299"/> +<polygon fill="#000000" stroke="#000000" points="80.0574,-315.1591 69.6015,-316.8693 78.8418,-322.0527 80.0574,-315.1591"/> +</g> +<!-- logger->config --> +<g id="edge4" class="edge"> +<title>logger->config</title> +<path fill="none" stroke="#000000" d="M69.6636,-304.9054C146.6435,-291.3317 334.3698,-258.2305 420.0048,-243.1308"/> +<polygon fill="#000000" stroke="#000000" points="420.6875,-246.5645 429.9277,-241.3811 419.4719,-239.6708 420.6875,-246.5645"/> +</g> +<!-- gzip --> +<g id="node4" class="node"> +<title>gzip</title> +<ellipse fill="none" stroke="#000000" cx="531.6651" cy="-102.393" rx="27.8286" ry="18"/> +<text text-anchor="middle" x="531.6651" y="-98.193" font-family="Times,serif" font-size="14.00" fill="#000000">gzip</text> +</g> +<!-- headers --> +<g id="node5" class="node"> +<title>headers</title> +<ellipse fill="none" stroke="#000000" cx="576.4118" cy="-235.393" rx="40.548" ry="18"/> +<text text-anchor="middle" x="576.4118" y="-231.193" font-family="Times,serif" font-size="14.00" fill="#000000">headers</text> +</g> +<!-- headers->config --> +<g id="edge3" class="edge"> +<title>headers->config</title> +<path fill="none" stroke="#000000" d="M535.788,-235.393C527.3742,-235.393 518.4534,-235.393 509.8639,-235.393"/> +<polygon fill="#000000" stroke="#000000" points="509.607,-231.8931 499.607,-235.393 509.607,-238.8931 509.607,-231.8931"/> +</g> +<!-- metrics --> +<g id="node6" class="node"> +<title>metrics</title> +<ellipse fill="none" stroke="#000000" cx="412.2429" cy="-93.4998" rx="39.4196" ry="18"/> +<text text-anchor="middle" x="412.2429" y="-89.2998" font-family="Times,serif" font-size="14.00" fill="#000000">metrics</text> +</g> +<!-- metrics->config --> +<g id="edge6" class="edge"> +<title>metrics->config</title> +<path fill="none" stroke="#000000" d="M418.7837,-111.4707C427.6313,-135.7792 443.4953,-179.3651 453.8554,-207.8292"/> +<polygon fill="#000000" stroke="#000000" points="450.6908,-209.3682 457.4,-217.5681 457.2687,-206.974 450.6908,-209.3682"/> +</g> +<!-- metrics->logger --> +<g id="edge5" class="edge"> +<title>metrics->logger</title> +<path fill="none" stroke="#000000" d="M387.5373,-107.7636C321.7958,-145.7194 142.5487,-249.2078 68.4432,-291.9926"/> +<polygon fill="#000000" stroke="#000000" points="66.4391,-289.1082 59.5289,-297.1393 69.9391,-295.1704 66.4391,-289.1082"/> +</g> +<!-- redis --> +<g id="node7" class="node"> +<title>redis</title> +<ellipse fill="none" stroke="#000000" cx="281.4734" cy="-18" rx="29.6127" ry="18"/> +<text text-anchor="middle" x="281.4734" y="-13.8" font-family="Times,serif" font-size="14.00" fill="#000000">redis</text> +</g> +<!-- redis->config --> +<g id="edge8" class="edge"> +<title>redis->config</title> +<path fill="none" stroke="#000000" d="M295.1841,-34.3398C326.9308,-72.174 405.6399,-165.9759 443.2445,-210.7914"/> +<polygon fill="#000000" stroke="#000000" points="440.6581,-213.1541 449.7672,-218.5648 446.0204,-208.6545 440.6581,-213.1541"/> +</g> +<!-- redis->logger --> +<g id="edge7" class="edge"> +<title>redis->logger</title> +<path fill="none" stroke="#000000" d="M267.9098,-34.1644C227.1471,-82.7435 105.5381,-227.6715 56.5241,-286.0841"/> +<polygon fill="#000000" stroke="#000000" points="53.5843,-284.1426 49.8376,-294.0528 58.9466,-288.6421 53.5843,-284.1426"/> +</g> +<!-- reload --> +<g id="node8" class="node"> +<title>reload</title> +<ellipse fill="none" stroke="#000000" cx="281.4734" cy="-452.786" rx="35.3315" ry="18"/> +<text text-anchor="middle" x="281.4734" y="-448.586" font-family="Times,serif" font-size="14.00" fill="#000000">reload</text> +</g> +<!-- reload->config --> +<g id="edge10" class="edge"> +<title>reload->config</title> +<path fill="none" stroke="#000000" d="M295.4842,-436.0885C327.4495,-397.9939 405.8819,-304.5217 443.3335,-259.8887"/> +<polygon fill="#000000" stroke="#000000" points="446.0824,-262.0576 449.8292,-252.1474 440.7201,-257.5581 446.0824,-262.0576"/> +</g> +<!-- reload->logger --> +<g id="edge9" class="edge"> +<title>reload->logger</title> +<path fill="none" stroke="#000000" d="M257.9083,-439.1807C213.6848,-413.6483 118.2025,-358.5216 68.0211,-329.5493"/> +<polygon fill="#000000" stroke="#000000" points="69.6111,-326.4259 59.2009,-324.457 66.1111,-332.4881 69.6111,-326.4259"/> +</g> +<!-- resetter --> +<g id="node9" class="node"> +<title>resetter</title> +<ellipse fill="none" stroke="#000000" cx="132.7678" cy="-426.5652" rx="39.3984" ry="18"/> +<text text-anchor="middle" x="132.7678" y="-422.3652" font-family="Times,serif" font-size="14.00" fill="#000000">resetter</text> +</g> +<!-- reload->resetter --> +<g id="edge11" class="edge"> +<title>reload->resetter</title> +<path fill="none" stroke="#000000" d="M248.1009,-446.9016C227.9026,-443.3401 201.8366,-438.7439 179.5962,-434.8224"/> +<polygon fill="#000000" stroke="#000000" points="180.1376,-431.3639 169.6817,-433.0742 178.922,-438.2575 180.1376,-431.3639"/> +</g> +<!-- resetter->logger --> +<g id="edge12" class="edge"> +<title>resetter->logger</title> +<path fill="none" stroke="#000000" d="M118.4461,-409.4974C102.0084,-389.9077 74.9173,-357.6218 56.2379,-335.3605"/> +<polygon fill="#000000" stroke="#000000" points="58.881,-333.0653 49.7719,-327.6546 53.5187,-337.5649 58.881,-333.0653"/> +</g> +<!-- rpc --> +<g id="node10" class="node"> +<title>rpc</title> +<ellipse fill="none" stroke="#000000" cx="132.7678" cy="-44.2208" rx="27" ry="18"/> +<text text-anchor="middle" x="132.7678" y="-40.0208" font-family="Times,serif" font-size="14.00" fill="#000000">rpc</text> +</g> +<!-- rpc->config --> +<g id="edge13" class="edge"> +<title>rpc->config</title> +<path fill="none" stroke="#000000" d="M153.4808,-56.1795C209.3277,-88.4227 363.359,-177.3527 431.1448,-216.4889"/> +<polygon fill="#000000" stroke="#000000" points="429.7078,-219.7006 440.1181,-221.6696 433.2078,-213.6384 429.7078,-219.7006"/> +</g> +<!-- rpc->logger --> +<g id="edge14" class="edge"> +<title>rpc->logger</title> +<path fill="none" stroke="#000000" d="M126.3994,-61.7179C109.8827,-107.097 65.5725,-228.8383 45.6502,-283.5745"/> +<polygon fill="#000000" stroke="#000000" points="42.3576,-282.3876 42.2262,-292.9816 48.9354,-284.7818 42.3576,-282.3876"/> +</g> +<!-- static --> +<g id="node11" class="node"> +<title>static</title> +<ellipse fill="none" stroke="#000000" cx="35.7071" cy="-159.8932" rx="31.3333" ry="18"/> +<text text-anchor="middle" x="35.7071" y="-155.6932" font-family="Times,serif" font-size="14.00" fill="#000000">static</text> +</g> +<!-- static->config --> +<g id="edge15" class="edge"> +<title>static->config</title> +<path fill="none" stroke="#000000" d="M65.8159,-165.2022C140.1736,-178.3135 332.7753,-212.2743 419.9157,-227.6396"/> +<polygon fill="#000000" stroke="#000000" points="419.5489,-231.1288 430.0048,-229.4185 420.7645,-224.2351 419.5489,-231.1288"/> +</g> +<!-- static->logger --> +<g id="edge16" class="edge"> +<title>static->logger</title> +<path fill="none" stroke="#000000" d="M35.7071,-178.1073C35.7071,-204.0691 35.7071,-251.9543 35.7071,-282.5696"/> +<polygon fill="#000000" stroke="#000000" points="32.2072,-282.6141 35.7071,-292.6141 39.2072,-282.6142 32.2072,-282.6141"/> +</g> +</g> +</svg>
\ No newline at end of file diff --git a/plugins/gzip/plugin.go b/plugins/gzip/plugin.go new file mode 100644 index 00000000..e5b9e4f5 --- /dev/null +++ b/plugins/gzip/plugin.go @@ -0,0 +1,25 @@ +package gzip + +import ( + "net/http" + + "github.com/NYTimes/gziphandler" +) + +const PluginName = "gzip" + +type Gzip struct{} + +func (g *Gzip) Init() error { + return nil +} + +func (g *Gzip) Middleware(next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + gziphandler.GzipHandler(next).ServeHTTP(w, r) + } +} + +func (g *Gzip) Name() string { + return PluginName +} diff --git a/plugins/headers/config.go b/plugins/headers/config.go new file mode 100644 index 00000000..8d4e29c2 --- /dev/null +++ b/plugins/headers/config.go @@ -0,0 +1,36 @@ +package headers + +// Config declares headers service configuration. +type Config struct { + Headers struct { + // CORS settings. + CORS *CORSConfig + + // Request headers to add to every payload send to PHP. + Request map[string]string + + // Response headers to add to every payload generated by PHP. + Response map[string]string + } +} + +// CORSConfig headers configuration. +type CORSConfig struct { + // AllowedOrigin: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + AllowedOrigin string + + // AllowedHeaders: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers + AllowedHeaders string + + // AllowedMethods: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods + AllowedMethods string + + // AllowCredentials https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials + AllowCredentials *bool + + // ExposeHeaders: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers + ExposedHeaders string + + // MaxAge of CORS headers in seconds/ + MaxAge int +} diff --git a/plugins/headers/plugin.go b/plugins/headers/plugin.go new file mode 100644 index 00000000..f1c6e6f3 --- /dev/null +++ b/plugins/headers/plugin.go @@ -0,0 +1,117 @@ +package headers + +import ( + "net/http" + "strconv" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" +) + +// ID contains default service name. +const PluginName = "headers" +const RootPluginName = "http" + +// Service serves headers files. Potentially convert into middleware? +type Plugin struct { + // server configuration (location, forbidden files and etc) + cfg *Config +} + +// Init must return configure service and return true if service hasStatus enabled. Must return error in case of +// misconfiguration. Services must not be used without proper configuration pushed first. +func (s *Plugin) Init(cfg config.Configurer) error { + const op = errors.Op("headers plugin init") + err := cfg.UnmarshalKey(RootPluginName, &s.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + return nil +} + +// middleware must return true if request/response pair is handled within the middleware. +func (s *Plugin) Middleware(next http.Handler) http.HandlerFunc { + // Define the http.HandlerFunc + return func(w http.ResponseWriter, r *http.Request) { + if s.cfg.Headers.Request != nil { + for k, v := range s.cfg.Headers.Request { + r.Header.Add(k, v) + } + } + + if s.cfg.Headers.Response != nil { + for k, v := range s.cfg.Headers.Response { + w.Header().Set(k, v) + } + } + + if s.cfg.Headers.CORS != nil { + if r.Method == http.MethodOptions { + s.preflightRequest(w) + return + } + s.corsHeaders(w) + } + + next.ServeHTTP(w, r) + } +} + +func (s *Plugin) Name() string { + return PluginName +} + +// configure OPTIONS response +func (s *Plugin) preflightRequest(w http.ResponseWriter) { + headers := w.Header() + + headers.Add("Vary", "Origin") + headers.Add("Vary", "Access-Control-Request-Method") + headers.Add("Vary", "Access-Control-Request-Headers") + + if s.cfg.Headers.CORS.AllowedOrigin != "" { + headers.Set("Access-Control-Allow-Origin", s.cfg.Headers.CORS.AllowedOrigin) + } + + if s.cfg.Headers.CORS.AllowedHeaders != "" { + headers.Set("Access-Control-Allow-Headers", s.cfg.Headers.CORS.AllowedHeaders) + } + + if s.cfg.Headers.CORS.AllowedMethods != "" { + headers.Set("Access-Control-Allow-Methods", s.cfg.Headers.CORS.AllowedMethods) + } + + if s.cfg.Headers.CORS.AllowCredentials != nil { + headers.Set("Access-Control-Allow-Credentials", strconv.FormatBool(*s.cfg.Headers.CORS.AllowCredentials)) + } + + if s.cfg.Headers.CORS.MaxAge > 0 { + headers.Set("Access-Control-Max-Age", strconv.Itoa(s.cfg.Headers.CORS.MaxAge)) + } + + w.WriteHeader(http.StatusOK) +} + +// configure CORS headers +func (s *Plugin) corsHeaders(w http.ResponseWriter) { + headers := w.Header() + + headers.Add("Vary", "Origin") + + if s.cfg.Headers.CORS.AllowedOrigin != "" { + headers.Set("Access-Control-Allow-Origin", s.cfg.Headers.CORS.AllowedOrigin) + } + + if s.cfg.Headers.CORS.AllowedHeaders != "" { + headers.Set("Access-Control-Allow-Headers", s.cfg.Headers.CORS.AllowedHeaders) + } + + if s.cfg.Headers.CORS.ExposedHeaders != "" { + headers.Set("Access-Control-Expose-Headers", s.cfg.Headers.CORS.ExposedHeaders) + } + + if s.cfg.Headers.CORS.AllowCredentials != nil { + headers.Set("Access-Control-Allow-Credentials", strconv.FormatBool(*s.cfg.Headers.CORS.AllowCredentials)) + } +} diff --git a/pkg/plugins/http/attributes/attributes.go b/plugins/http/attributes/attributes.go index 4c453766..4c453766 100644 --- a/pkg/plugins/http/attributes/attributes.go +++ b/plugins/http/attributes/attributes.go diff --git a/pkg/plugins/http/config.go b/plugins/http/config.go index 00d2940b..00d2940b 100644 --- a/pkg/plugins/http/config.go +++ b/plugins/http/config.go diff --git a/pkg/plugins/http/constants.go b/plugins/http/constants.go index 773d1f46..773d1f46 100644 --- a/pkg/plugins/http/constants.go +++ b/plugins/http/constants.go diff --git a/pkg/plugins/http/errors.go b/plugins/http/errors.go index fb8762ef..fb8762ef 100644 --- a/pkg/plugins/http/errors.go +++ b/plugins/http/errors.go diff --git a/pkg/plugins/http/errors_windows.go b/plugins/http/errors_windows.go index 3d0ba04c..3d0ba04c 100644 --- a/pkg/plugins/http/errors_windows.go +++ b/plugins/http/errors_windows.go diff --git a/pkg/plugins/http/handler.go b/plugins/http/handler.go index 08cee661..1889ed6d 100644 --- a/pkg/plugins/http/handler.go +++ b/plugins/http/handler.go @@ -10,9 +10,9 @@ import ( "github.com/hashicorp/go-multierror" "github.com/spiral/errors" - "github.com/spiral/roadrunner-plugins/logger" "github.com/spiral/roadrunner/v2/interfaces/events" "github.com/spiral/roadrunner/v2/interfaces/pool" + "github.com/spiral/roadrunner/v2/plugins/logger" ) const ( diff --git a/pkg/plugins/http/parse.go b/plugins/http/parse.go index d4a1604b..d4a1604b 100644 --- a/pkg/plugins/http/parse.go +++ b/plugins/http/parse.go diff --git a/pkg/plugins/http/plugin.go b/plugins/http/plugin.go index d8ca6466..f3708ced 100644 --- a/pkg/plugins/http/plugin.go +++ b/plugins/http/plugin.go @@ -15,14 +15,14 @@ import ( "github.com/hashicorp/go-multierror" "github.com/spiral/endure" "github.com/spiral/errors" - "github.com/spiral/roadrunner-plugins/checker" - "github.com/spiral/roadrunner-plugins/config" - "github.com/spiral/roadrunner-plugins/logger" "github.com/spiral/roadrunner/v2/interfaces/pool" "github.com/spiral/roadrunner/v2/interfaces/worker" - "github.com/spiral/roadrunner/v2/pkg/plugins/http/attributes" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + "github.com/spiral/roadrunner/v2/plugins/checker" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/http/attributes" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" "github.com/spiral/roadrunner/v2/util" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" diff --git a/pkg/plugins/http/request.go b/plugins/http/request.go index 9da05fd8..3983fdde 100644 --- a/pkg/plugins/http/request.go +++ b/plugins/http/request.go @@ -9,9 +9,9 @@ import ( "strings" j "github.com/json-iterator/go" - "github.com/spiral/roadrunner-plugins/logger" "github.com/spiral/roadrunner/v2/pkg/payload" - "github.com/spiral/roadrunner/v2/pkg/plugins/http/attributes" + "github.com/spiral/roadrunner/v2/plugins/http/attributes" + "github.com/spiral/roadrunner/v2/plugins/logger" ) var json = j.ConfigCompatibleWithStandardLibrary diff --git a/pkg/plugins/http/response.go b/plugins/http/response.go index 17049ce1..17049ce1 100644 --- a/pkg/plugins/http/response.go +++ b/plugins/http/response.go diff --git a/pkg/plugins/http/uploads.go b/plugins/http/uploads.go index 1f14cc0d..d5196844 100644 --- a/pkg/plugins/http/uploads.go +++ b/plugins/http/uploads.go @@ -1,7 +1,7 @@ package http import ( - "github.com/spiral/roadrunner-plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/logger" "io" "io/ioutil" diff --git a/pkg/plugins/http/uploads_config.go b/plugins/http/uploads_config.go index 4c20c8e8..4c20c8e8 100644 --- a/pkg/plugins/http/uploads_config.go +++ b/plugins/http/uploads_config.go diff --git a/pkg/plugins/informer/interface.go b/plugins/informer/interface.go index 27139ae1..27139ae1 100644 --- a/pkg/plugins/informer/interface.go +++ b/plugins/informer/interface.go diff --git a/pkg/plugins/informer/plugin.go b/plugins/informer/plugin.go index e2da7d86..3359cd7e 100644 --- a/pkg/plugins/informer/plugin.go +++ b/plugins/informer/plugin.go @@ -3,8 +3,8 @@ package informer import ( "github.com/spiral/endure" "github.com/spiral/errors" - "github.com/spiral/roadrunner-plugins/logger" "github.com/spiral/roadrunner/v2/interfaces/worker" + "github.com/spiral/roadrunner/v2/plugins/logger" ) const PluginName = "informer" diff --git a/pkg/plugins/informer/rpc.go b/plugins/informer/rpc.go index d32d4e3a..98b5681c 100644 --- a/pkg/plugins/informer/rpc.go +++ b/plugins/informer/rpc.go @@ -1,8 +1,8 @@ package informer import ( - "github.com/spiral/roadrunner-plugins/logger" "github.com/spiral/roadrunner/v2/interfaces/worker" + "github.com/spiral/roadrunner/v2/plugins/logger" "github.com/spiral/roadrunner/v2/tools" ) diff --git a/plugins/logger/config.go b/plugins/logger/config.go new file mode 100644 index 00000000..f7a5742c --- /dev/null +++ b/plugins/logger/config.go @@ -0,0 +1,94 @@ +package logger + +import ( + "strings" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// ChannelConfig configures loggers per channel. +type ChannelConfig struct { + // Dedicated channels per logger. By default logger allocated via named logger. + Channels map[string]Config `json:"channels" yaml:"channels"` +} + +type Config struct { + // Mode configures logger based on some default template (development, production, off). + Mode string `json:"mode" yaml:"mode"` + + // Level is the minimum enabled logging level. Note that this is a dynamic + // level, so calling ChannelConfig.Level.SetLevel will atomically change the log + // level of all loggers descended from this config. + Level string `json:"level" yaml:"level"` + + // Encoding sets the logger's encoding. Valid values are "json" and + // "console", as well as any third-party encodings registered via + // RegisterEncoder. + Encoding string `json:"encoding" yaml:"encoding"` + + // Output is a list of URLs or file paths to write logging output to. + // See Open for details. + Output []string `json:"output" yaml:"output"` + + // ErrorOutput is a list of URLs to write internal logger errors to. + // The default is standard error. + // + // Note that this setting only affects internal errors; for sample code that + // sends error-level logs to a different location from info- and debug-level + // logs, see the package-level AdvancedConfiguration example. + ErrorOutput []string `json:"errorOutput" yaml:"errorOutput"` +} + +// ZapConfig converts config into Zap configuration. +func (cfg *Config) BuildLogger() (*zap.Logger, error) { + var zCfg zap.Config + switch strings.ToLower(cfg.Mode) { + case "off", "none": + return zap.NewNop(), nil + case "production": + zCfg = zap.NewProductionConfig() + case "development": + zCfg = zap.NewDevelopmentConfig() + default: + zCfg = zap.Config{ + Level: zap.NewAtomicLevelAt(zap.DebugLevel), + Encoding: "console", + EncoderConfig: zapcore.EncoderConfig{ + MessageKey: "message", + LevelKey: "level", + TimeKey: "time", + NameKey: "name", + EncodeName: ColoredHashedNameEncoder, + EncodeLevel: ColoredLevelEncoder, + EncodeTime: UTCTimeEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + }, + OutputPaths: []string{"stderr"}, + ErrorOutputPaths: []string{"stderr"}, + } + } + + if cfg.Level != "" { + level := zap.NewAtomicLevel() + if err := level.UnmarshalText([]byte(cfg.Level)); err == nil { + zCfg.Level = level + } + } + + if cfg.Encoding != "" { + zCfg.Encoding = cfg.Encoding + } + + if len(cfg.Output) != 0 { + zCfg.OutputPaths = cfg.Output + } + + if len(cfg.ErrorOutput) != 0 { + zCfg.ErrorOutputPaths = cfg.ErrorOutput + } + + // todo: https://github.com/uber-go/zap/blob/master/FAQ.md#does-zap-support-log-rotation + + return zCfg.Build() +} diff --git a/plugins/logger/encoder.go b/plugins/logger/encoder.go new file mode 100644 index 00000000..4ff583c4 --- /dev/null +++ b/plugins/logger/encoder.go @@ -0,0 +1,66 @@ +package logger + +import ( + "hash/fnv" + "strings" + "time" + + "github.com/fatih/color" + "go.uber.org/zap/zapcore" +) + +var colorMap = []func(string, ...interface{}) string{ + color.HiYellowString, + color.HiGreenString, + color.HiBlueString, + color.HiRedString, + color.HiCyanString, + color.HiMagentaString, +} + +// ColoredLevelEncoder colorizes log levels. +func ColoredLevelEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { + switch level { + case zapcore.DebugLevel: + enc.AppendString(color.HiWhiteString(level.CapitalString())) + case zapcore.InfoLevel: + enc.AppendString(color.HiCyanString(level.CapitalString())) + case zapcore.WarnLevel: + enc.AppendString(color.HiYellowString(level.CapitalString())) + case zapcore.ErrorLevel, zapcore.DPanicLevel: + enc.AppendString(color.HiRedString(level.CapitalString())) + case zapcore.PanicLevel, zapcore.FatalLevel: + enc.AppendString(color.HiMagentaString(level.CapitalString())) + } +} + +// ColoredNameEncoder colorizes service names. +func ColoredNameEncoder(s string, enc zapcore.PrimitiveArrayEncoder) { + if len(s) < 12 { + s += strings.Repeat(" ", 12-len(s)) + } + + enc.AppendString(color.HiGreenString(s)) +} + +// ColoredHashedNameEncoder colorizes service names and assigns different colors to different names. +func ColoredHashedNameEncoder(s string, enc zapcore.PrimitiveArrayEncoder) { + if len(s) < 12 { + s += strings.Repeat(" ", 12-len(s)) + } + + colorID := stringHash(s, len(colorMap)) + enc.AppendString(colorMap[colorID](s)) +} + +// UTCTimeEncoder encodes time into short UTC specific timestamp. +func UTCTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.UTC().Format("2006/01/02 15:04:05")) +} + +// returns string hash +func stringHash(name string, base int) int { + h := fnv.New32a() + _, _ = h.Write([]byte(name)) + return int(h.Sum32()) % base +} diff --git a/plugins/logger/interface.go b/plugins/logger/interface.go new file mode 100644 index 00000000..876629a9 --- /dev/null +++ b/plugins/logger/interface.go @@ -0,0 +1,16 @@ +package logger + +type ( + // Logger is an general RR log interface + Logger interface { + Debug(msg string, keyvals ...interface{}) + Info(msg string, keyvals ...interface{}) + Warn(msg string, keyvals ...interface{}) + Error(msg string, keyvals ...interface{}) + } +) + +// With creates a child logger and adds structured context to it +type WithLogger interface { + With(keyvals ...interface{}) Logger +} diff --git a/plugins/logger/plugin.go b/plugins/logger/plugin.go new file mode 100644 index 00000000..01bf5cc0 --- /dev/null +++ b/plugins/logger/plugin.go @@ -0,0 +1,69 @@ +package logger + +import ( + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "go.uber.org/zap" +) + +// PluginName declares plugin name. +const PluginName = "logs" + +// ZapLogger manages zap logger. +type ZapLogger struct { + base *zap.Logger + cfg Config + channels ChannelConfig +} + +// Init logger service. +func (z *ZapLogger) Init(cfg config.Configurer) error { + const op = errors.Op("zap logger init") + err := cfg.UnmarshalKey(PluginName, &z.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + err = cfg.UnmarshalKey(PluginName, &z.channels) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + z.base, err = z.cfg.BuildLogger() + if err != nil { + return errors.E(op, errors.Disabled, err) + } + return nil +} + +// DefaultLogger returns default logger. +func (z *ZapLogger) DefaultLogger() (Logger, error) { + return NewZapAdapter(z.base), nil +} + +// NamedLogger returns logger dedicated to the specific channel. Similar to Named() but also reads the core params. +func (z *ZapLogger) NamedLogger(name string) (Logger, error) { + if cfg, ok := z.channels.Channels[name]; ok { + l, err := cfg.BuildLogger() + if err != nil { + return nil, err + } + return NewZapAdapter(l), nil + } + + return NewZapAdapter(z.base.Named(name)), nil +} + +// NamedLogger returns logger dedicated to the specific channel. Similar to Named() but also reads the core params. +func (z *ZapLogger) ServiceLogger(n endure.Named) (Logger, error) { + return z.NamedLogger(n.Name()) +} + +// Provides declares factory methods. +func (z *ZapLogger) Provides() []interface{} { + return []interface{}{ + z.ServiceLogger, + z.DefaultLogger, + } +} diff --git a/plugins/logger/zap_adapter.go b/plugins/logger/zap_adapter.go new file mode 100644 index 00000000..0a0855b8 --- /dev/null +++ b/plugins/logger/zap_adapter.go @@ -0,0 +1,56 @@ +package logger + +import ( + "fmt" + + "go.uber.org/zap" +) + +type ZapAdapter struct { + zl *zap.Logger +} + +// Create NewZapAdapter which uses general log interface +func NewZapAdapter(zapLogger *zap.Logger) *ZapAdapter { + return &ZapAdapter{ + zl: zapLogger.WithOptions(zap.AddCallerSkip(1)), + } +} + +func (log *ZapAdapter) fields(keyvals []interface{}) []zap.Field { + // we should have even number of keys and values + if len(keyvals)%2 != 0 { + return []zap.Field{zap.Error(fmt.Errorf("odd number of keyvals pairs: %v", keyvals))} + } + + var fields []zap.Field + for i := 0; i < len(keyvals); i += 2 { + key, ok := keyvals[i].(string) + if !ok { + key = fmt.Sprintf("%v", keyvals[i]) + } + fields = append(fields, zap.Any(key, keyvals[i+1])) + } + + return fields +} + +func (log *ZapAdapter) Debug(msg string, keyvals ...interface{}) { + log.zl.Debug(msg, log.fields(keyvals)...) +} + +func (log *ZapAdapter) Info(msg string, keyvals ...interface{}) { + log.zl.Info(msg, log.fields(keyvals)...) +} + +func (log *ZapAdapter) Warn(msg string, keyvals ...interface{}) { + log.zl.Warn(msg, log.fields(keyvals)...) +} + +func (log *ZapAdapter) Error(msg string, keyvals ...interface{}) { + log.zl.Error(msg, log.fields(keyvals)...) +} + +func (log *ZapAdapter) With(keyvals ...interface{}) Logger { + return NewZapAdapter(log.zl.With(log.fields(keyvals)...)) +} diff --git a/plugins/metrics/config.go b/plugins/metrics/config.go new file mode 100644 index 00000000..9459bc9b --- /dev/null +++ b/plugins/metrics/config.go @@ -0,0 +1,138 @@ +package metrics + +import ( + "fmt" + + "github.com/prometheus/client_golang/prometheus" +) + +// Config configures metrics service. +type Config struct { + // Address to listen + Address string + + // Collect define application specific metrics. + Collect map[string]Collector +} + +type NamedCollector struct { + // Name of the collector + Name string `json:"name"` + + // Collector structure + Collector `json:"collector"` +} + +// CollectorType represents prometheus collector types +type CollectorType string + +const ( + // Histogram type + Histogram CollectorType = "histogram" + + // Gauge type + Gauge CollectorType = "gauge" + + // Counter type + Counter CollectorType = "counter" + + // Summary type + Summary CollectorType = "summary" +) + +// Collector describes single application specific metric. +type Collector struct { + // Namespace of the metric. + Namespace string `json:"namespace"` + // Subsystem of the metric. + Subsystem string `json:"subsystem"` + // Collector type (histogram, gauge, counter, summary). + Type CollectorType `json:"type"` + // Help of collector. + Help string `json:"help"` + // Labels for vectorized metrics. + Labels []string `json:"labels"` + // Buckets for histogram metric. + Buckets []float64 `json:"buckets"` + // Objectives for the summary opts + Objectives map[float64]float64 `json:"objectives"` +} + +// register application specific metrics. +func (c *Config) getCollectors() (map[string]prometheus.Collector, error) { + if c.Collect == nil { + return nil, nil + } + + collectors := make(map[string]prometheus.Collector) + + for name, m := range c.Collect { + var collector prometheus.Collector + switch m.Type { + case Histogram: + opts := prometheus.HistogramOpts{ + Name: name, + Namespace: m.Namespace, + Subsystem: m.Subsystem, + Help: m.Help, + Buckets: m.Buckets, + } + + if len(m.Labels) != 0 { + collector = prometheus.NewHistogramVec(opts, m.Labels) + } else { + collector = prometheus.NewHistogram(opts) + } + case Gauge: + opts := prometheus.GaugeOpts{ + Name: name, + Namespace: m.Namespace, + Subsystem: m.Subsystem, + Help: m.Help, + } + + if len(m.Labels) != 0 { + collector = prometheus.NewGaugeVec(opts, m.Labels) + } else { + collector = prometheus.NewGauge(opts) + } + case Counter: + opts := prometheus.CounterOpts{ + Name: name, + Namespace: m.Namespace, + Subsystem: m.Subsystem, + Help: m.Help, + } + + if len(m.Labels) != 0 { + collector = prometheus.NewCounterVec(opts, m.Labels) + } else { + collector = prometheus.NewCounter(opts) + } + case Summary: + opts := prometheus.SummaryOpts{ + Name: name, + Namespace: m.Namespace, + Subsystem: m.Subsystem, + Help: m.Help, + Objectives: m.Objectives, + } + + if len(m.Labels) != 0 { + collector = prometheus.NewSummaryVec(opts, m.Labels) + } else { + collector = prometheus.NewSummary(opts) + } + default: + return nil, fmt.Errorf("invalid metric type `%s` for `%s`", m.Type, name) + } + + collectors[name] = collector + } + + return collectors, nil +} + +func (c *Config) InitDefaults() { + +} diff --git a/plugins/metrics/config_test.go b/plugins/metrics/config_test.go new file mode 100644 index 00000000..665ec9cd --- /dev/null +++ b/plugins/metrics/config_test.go @@ -0,0 +1,89 @@ +package metrics + +import ( + "bytes" + "testing" + + j "github.com/json-iterator/go" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" +) + +var json = j.ConfigCompatibleWithStandardLibrary + +func Test_Config_Hydrate_Error1(t *testing.T) { + cfg := `{"request": {"From": "Something"}}` + c := &Config{} + f := new(bytes.Buffer) + f.WriteString(cfg) + + err := json.Unmarshal(f.Bytes(), &c) + if err != nil { + t.Fatal(err) + } +} + +func Test_Config_Hydrate_Error2(t *testing.T) { + cfg := `{"dir": "/dir/"` + c := &Config{} + + f := new(bytes.Buffer) + f.WriteString(cfg) + + err := json.Unmarshal(f.Bytes(), &c) + assert.Error(t, err) +} + +func Test_Config_Metrics(t *testing.T) { + cfg := `{ +"collect":{ + "metric1":{"type": "gauge"}, + "metric2":{ "type": "counter"}, + "metric3":{"type": "summary"}, + "metric4":{"type": "histogram"} +} +}` + c := &Config{} + f := new(bytes.Buffer) + f.WriteString(cfg) + + err := json.Unmarshal(f.Bytes(), &c) + if err != nil { + t.Fatal(err) + } + + m, err := c.getCollectors() + assert.NoError(t, err) + + assert.IsType(t, prometheus.NewGauge(prometheus.GaugeOpts{}), m["metric1"]) + assert.IsType(t, prometheus.NewCounter(prometheus.CounterOpts{}), m["metric2"]) + assert.IsType(t, prometheus.NewSummary(prometheus.SummaryOpts{}), m["metric3"]) + assert.IsType(t, prometheus.NewHistogram(prometheus.HistogramOpts{}), m["metric4"]) +} + +func Test_Config_MetricsVector(t *testing.T) { + cfg := `{ +"collect":{ + "metric1":{"type": "gauge","labels":["label"]}, + "metric2":{ "type": "counter","labels":["label"]}, + "metric3":{"type": "summary","labels":["label"]}, + "metric4":{"type": "histogram","labels":["label"]} +} +}` + c := &Config{} + f := new(bytes.Buffer) + f.WriteString(cfg) + + err := json.Unmarshal(f.Bytes(), &c) + if err != nil { + t.Fatal(err) + } + + m, err := c.getCollectors() + assert.NoError(t, err) + + assert.IsType(t, prometheus.NewGaugeVec(prometheus.GaugeOpts{}, []string{}), m["metric1"]) + assert.IsType(t, prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{}), m["metric2"]) + assert.IsType(t, prometheus.NewSummaryVec(prometheus.SummaryOpts{}, []string{}), m["metric3"]) + assert.IsType(t, prometheus.NewHistogramVec(prometheus.HistogramOpts{}, []string{}), m["metric4"]) +} diff --git a/plugins/metrics/doc.go b/plugins/metrics/doc.go new file mode 100644 index 00000000..1abe097a --- /dev/null +++ b/plugins/metrics/doc.go @@ -0,0 +1 @@ +package metrics diff --git a/plugins/metrics/interface.go b/plugins/metrics/interface.go new file mode 100644 index 00000000..87ba4017 --- /dev/null +++ b/plugins/metrics/interface.go @@ -0,0 +1,7 @@ +package metrics + +import "github.com/prometheus/client_golang/prometheus" + +type StatProvider interface { + MetricsCollector() []prometheus.Collector +} diff --git a/plugins/metrics/plugin.go b/plugins/metrics/plugin.go new file mode 100644 index 00000000..fb9096a1 --- /dev/null +++ b/plugins/metrics/plugin.go @@ -0,0 +1,229 @@ +package metrics + +import ( + "context" + "crypto/tls" + "net/http" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "golang.org/x/sys/cpu" +) + +const ( + // PluginName declares plugin name. + PluginName = "metrics" + // maxHeaderSize declares max header size for prometheus server + maxHeaderSize = 1024 * 1024 * 100 // 104MB +) + +type statsProvider struct { + collectors []prometheus.Collector + name string +} + +// Plugin to manage application metrics using Prometheus. +type Plugin struct { + cfg Config + log logger.Logger + mu sync.Mutex // all receivers are pointers + http *http.Server + collectors sync.Map // all receivers are pointers + registry *prometheus.Registry +} + +// Init service. +func (m *Plugin) Init(cfg config.Configurer, log logger.Logger) error { + const op = errors.Op("metrics init") + err := cfg.UnmarshalKey(PluginName, &m.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + // TODO figure out what is Init + m.cfg.InitDefaults() + + m.log = log + m.registry = prometheus.NewRegistry() + + // Default + err = m.registry.Register(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{})) + if err != nil { + return errors.E(op, err) + } + + // Default + err = m.registry.Register(prometheus.NewGoCollector()) + if err != nil { + return errors.E(op, err) + } + + collectors, err := m.cfg.getCollectors() + if err != nil { + return errors.E(op, err) + } + + // Register invocation will be later in the Serve method + for k, v := range collectors { + m.collectors.Store(k, statsProvider{ + collectors: []prometheus.Collector{v}, + name: k, + }) + } + return nil +} + +// Register new prometheus collector. +func (m *Plugin) Register(c prometheus.Collector) error { + return m.registry.Register(c) +} + +// Serve prometheus metrics service. +func (m *Plugin) Serve() chan error { + errCh := make(chan error, 1) + m.collectors.Range(func(key, value interface{}) bool { + // key - name + // value - statsProvider struct + c := value.(statsProvider) + for _, v := range c.collectors { + if err := m.registry.Register(v); err != nil { + errCh <- err + return false + } + } + + return true + }) + + var topCipherSuites []uint16 + var defaultCipherSuitesTLS13 []uint16 + + hasGCMAsmAMD64 := cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ + hasGCMAsmARM64 := cpu.ARM64.HasAES && cpu.ARM64.HasPMULL + // Keep in sync with crypto/aes/cipher_s390x.go. + hasGCMAsmS390X := cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasAESCTR && (cpu.S390X.HasGHASH || cpu.S390X.HasAESGCM) + + hasGCMAsm := hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X + + if hasGCMAsm { + // If AES-GCM hardware is provided then prioritise AES-GCM + // cipher suites. + topCipherSuites = []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + } + defaultCipherSuitesTLS13 = []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_AES_256_GCM_SHA384, + } + } else { + // Without AES-GCM hardware, we put the ChaCha20-Poly1305 + // cipher suites first. + topCipherSuites = []uint16{ + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + } + defaultCipherSuitesTLS13 = []uint16{ + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + } + } + + DefaultCipherSuites := make([]uint16, 0, 22) + DefaultCipherSuites = append(DefaultCipherSuites, topCipherSuites...) + DefaultCipherSuites = append(DefaultCipherSuites, defaultCipherSuitesTLS13...) + + m.http = &http.Server{ + Addr: m.cfg.Address, + Handler: promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{}), + IdleTimeout: time.Hour * 24, + ReadTimeout: time.Minute * 60, + MaxHeaderBytes: maxHeaderSize, + ReadHeaderTimeout: time.Minute * 60, + WriteTimeout: time.Minute * 60, + TLSConfig: &tls.Config{ + CurvePreferences: []tls.CurveID{ + tls.CurveP256, + tls.CurveP384, + tls.CurveP521, + tls.X25519, + }, + CipherSuites: DefaultCipherSuites, + MinVersion: tls.VersionTLS12, + PreferServerCipherSuites: true, + }, + } + + go func() { + err := m.http.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + errCh <- err + return + } + }() + + return errCh +} + +// Stop prometheus metrics service. +func (m *Plugin) Stop() error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.http != nil { + // timeout is 10 seconds + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + err := m.http.Shutdown(ctx) + if err != nil { + // Function should be Stop() error + m.log.Error("stop error", "error", errors.Errorf("error shutting down the metrics server: error %v", err)) + } + } + return nil +} + +// Collects used to collect all plugins which implement metrics.StatProvider interface (and Named) +func (m *Plugin) Collects() []interface{} { + return []interface{}{ + m.AddStatProvider, + } +} + +// Collector returns application specific collector by name or nil if collector not found. +func (m *Plugin) AddStatProvider(name endure.Named, stat StatProvider) error { + m.collectors.Store(name.Name(), statsProvider{ + collectors: stat.MetricsCollector(), + name: name.Name(), + }) + return nil +} + +// RPC interface satisfaction +func (m *Plugin) Name() string { + return PluginName +} + +// RPC interface satisfaction +func (m *Plugin) RPC() interface{} { + return &rpcServer{ + svc: m, + log: m.log, + } +} diff --git a/plugins/metrics/rpc.go b/plugins/metrics/rpc.go new file mode 100644 index 00000000..f9c6accb --- /dev/null +++ b/plugins/metrics/rpc.go @@ -0,0 +1,294 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +type rpcServer struct { + svc *Plugin + log logger.Logger +} + +// Metric represent single metric produced by the application. +type Metric struct { + // Collector name. + Name string + + // Collector value. + Value float64 + + // Labels associated with metric. Only for vector metrics. Must be provided in a form of label values. + Labels []string +} + +// Add new metric to the designated collector. +func (rpc *rpcServer) Add(m *Metric, ok *bool) error { + const op = errors.Op("Add metric") + rpc.log.Info("Adding metric", "name", m.Name, "value", m.Value, "labels", m.Labels) + c, exist := rpc.svc.collectors.Load(m.Name) + if !exist { + rpc.log.Error("undefined collector", "collector", m.Name) + return errors.E(op, errors.Errorf("undefined collector %s, try first Declare the desired collector", m.Name)) + } + + switch c := c.(type) { + case prometheus.Gauge: + c.Add(m.Value) + + case *prometheus.GaugeVec: + if len(m.Labels) == 0 { + rpc.log.Error("required labels for collector", "collector", m.Name) + return errors.E(op, errors.Errorf("required labels for collector %s", m.Name)) + } + + gauge, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) + return errors.E(op, err) + } + gauge.Add(m.Value) + case prometheus.Counter: + c.Add(m.Value) + + case *prometheus.CounterVec: + if len(m.Labels) == 0 { + return errors.E(op, errors.Errorf("required labels for collector `%s`", m.Name)) + } + + gauge, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) + return errors.E(op, err) + } + gauge.Add(m.Value) + + default: + return errors.E(op, errors.Errorf("collector %s does not support method `Add`", m.Name)) + } + + // RPC, set ok to true as return value. Need by rpc.Call reply argument + *ok = true + rpc.log.Info("new metric successfully added", "name", m.Name, "labels", m.Labels, "value", m.Value) + return nil +} + +// Sub subtract the value from the specific metric (gauge only). +func (rpc *rpcServer) Sub(m *Metric, ok *bool) error { + const op = errors.Op("Subtracting metric") + rpc.log.Info("Subtracting value from metric", "name", m.Name, "value", m.Value, "labels", m.Labels) + c, exist := rpc.svc.collectors.Load(m.Name) + if !exist { + rpc.log.Error("undefined collector", "name", m.Name, "value", m.Value, "labels", m.Labels) + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + if c == nil { + // can it be nil ??? I guess can't + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + + switch c := c.(type) { + case prometheus.Gauge: + c.Sub(m.Value) + + case *prometheus.GaugeVec: + if len(m.Labels) == 0 { + rpc.log.Error("required labels for collector, but none was provided", "name", m.Name, "value", m.Value) + return errors.E(op, errors.Errorf("required labels for collector %s", m.Name)) + } + + gauge, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) + return errors.E(op, err) + } + gauge.Sub(m.Value) + default: + return errors.E(op, errors.Errorf("collector `%s` does not support method `Sub`", m.Name)) + } + rpc.log.Info("Subtracting operation applied successfully", "name", m.Name, "labels", m.Labels, "value", m.Value) + + *ok = true + return nil +} + +// Observe the value (histogram and summary only). +func (rpc *rpcServer) Observe(m *Metric, ok *bool) error { + const op = errors.Op("Observe metrics") + rpc.log.Info("Observing metric", "name", m.Name, "value", m.Value, "labels", m.Labels) + + c, exist := rpc.svc.collectors.Load(m.Name) + if !exist { + rpc.log.Error("undefined collector", "name", m.Name, "value", m.Value, "labels", m.Labels) + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + if c == nil { + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + + switch c := c.(type) { + case *prometheus.SummaryVec: + if len(m.Labels) == 0 { + return errors.E(op, errors.Errorf("required labels for collector `%s`", m.Name)) + } + + observer, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + return errors.E(op, err) + } + observer.Observe(m.Value) + + case prometheus.Histogram: + c.Observe(m.Value) + + case *prometheus.HistogramVec: + if len(m.Labels) == 0 { + return errors.E(op, errors.Errorf("required labels for collector `%s`", m.Name)) + } + + observer, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) + return errors.E(op, err) + } + observer.Observe(m.Value) + default: + return errors.E(op, errors.Errorf("collector `%s` does not support method `Observe`", m.Name)) + } + + rpc.log.Info("observe operation finished successfully", "name", m.Name, "labels", m.Labels, "value", m.Value) + + *ok = true + return nil +} + +// Declare is used to register new collector in prometheus +// THE TYPES ARE: +// NamedCollector -> Collector with the name +// bool -> RPC reply value +// RETURNS: +// error +func (rpc *rpcServer) Declare(nc *NamedCollector, ok *bool) error { + const op = errors.Op("Declare metric") + rpc.log.Info("Declaring new metric", "name", nc.Name, "type", nc.Type, "namespace", nc.Namespace) + _, exist := rpc.svc.collectors.Load(nc.Name) + if exist { + rpc.log.Error("metric with provided name already exist", "name", nc.Name, "type", nc.Type, "namespace", nc.Namespace) + return errors.E(op, errors.Errorf("tried to register existing collector with the name `%s`", nc.Name)) + } + + var collector prometheus.Collector + switch nc.Type { + case Histogram: + opts := prometheus.HistogramOpts{ + Name: nc.Name, + Namespace: nc.Namespace, + Subsystem: nc.Subsystem, + Help: nc.Help, + Buckets: nc.Buckets, + } + + if len(nc.Labels) != 0 { + collector = prometheus.NewHistogramVec(opts, nc.Labels) + } else { + collector = prometheus.NewHistogram(opts) + } + case Gauge: + opts := prometheus.GaugeOpts{ + Name: nc.Name, + Namespace: nc.Namespace, + Subsystem: nc.Subsystem, + Help: nc.Help, + } + + if len(nc.Labels) != 0 { + collector = prometheus.NewGaugeVec(opts, nc.Labels) + } else { + collector = prometheus.NewGauge(opts) + } + case Counter: + opts := prometheus.CounterOpts{ + Name: nc.Name, + Namespace: nc.Namespace, + Subsystem: nc.Subsystem, + Help: nc.Help, + } + + if len(nc.Labels) != 0 { + collector = prometheus.NewCounterVec(opts, nc.Labels) + } else { + collector = prometheus.NewCounter(opts) + } + case Summary: + opts := prometheus.SummaryOpts{ + Name: nc.Name, + Namespace: nc.Namespace, + Subsystem: nc.Subsystem, + Help: nc.Help, + } + + if len(nc.Labels) != 0 { + collector = prometheus.NewSummaryVec(opts, nc.Labels) + } else { + collector = prometheus.NewSummary(opts) + } + + default: + return errors.E(op, errors.Errorf("unknown collector type %s", nc.Type)) + } + + // add collector to sync.Map + rpc.svc.collectors.Store(nc.Name, collector) + // that method might panic, we handle it by recover + err := rpc.svc.Register(collector) + if err != nil { + *ok = false + return errors.E(op, err) + } + + rpc.log.Info("metric successfully added", "name", nc.Name, "type", nc.Type, "namespace", nc.Namespace) + + *ok = true + return nil +} + +// Set the metric value (only for gaude). +func (rpc *rpcServer) Set(m *Metric, ok *bool) (err error) { + const op = errors.Op("Set metric") + rpc.log.Info("Observing metric", "name", m.Name, "value", m.Value, "labels", m.Labels) + + c, exist := rpc.svc.collectors.Load(m.Name) + if !exist { + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + if c == nil { + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + + switch c := c.(type) { + case prometheus.Gauge: + c.Set(m.Value) + + case *prometheus.GaugeVec: + if len(m.Labels) == 0 { + rpc.log.Error("required labels for collector", "collector", m.Name) + return errors.E(op, errors.Errorf("required labels for collector %s", m.Name)) + } + + gauge, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) + return errors.E(op, err) + } + gauge.Set(m.Value) + + default: + return errors.E(op, errors.Errorf("collector `%s` does not support method Set", m.Name)) + } + + rpc.log.Info("set operation finished successfully", "name", m.Name, "labels", m.Labels, "value", m.Value) + + *ok = true + return nil +} diff --git a/plugins/redis/config.go b/plugins/redis/config.go new file mode 100644 index 00000000..ebcefed1 --- /dev/null +++ b/plugins/redis/config.go @@ -0,0 +1,32 @@ +package redis + +import "time" + +type Config struct { + Addrs []string `yaml:"addrs"` + DB int `yaml:"db"` + Username string `yaml:"username"` + Password string `yaml:"password"` + MasterName string `yaml:"master_name"` + SentinelPassword string `yaml:"sentinel_password"` + RouteByLatency bool `yaml:"route_by_latency"` + RouteRandomly bool `yaml:"route_randomly"` + MaxRetries int `yaml:"max_retries"` + DialTimeout time.Duration `yaml:"dial_timeout"` + MinRetryBackoff time.Duration `yaml:"min_retry_backoff"` + MaxRetryBackoff time.Duration `yaml:"max_retry_backoff"` + PoolSize int `yaml:"pool_size"` + MinIdleConns int `yaml:"min_idle_conns"` + MaxConnAge time.Duration `yaml:"max_conn_age"` + ReadTimeout time.Duration `yaml:"read_timeout"` + WriteTimeout time.Duration `yaml:"write_timeout"` + PoolTimeout time.Duration `yaml:"pool_timeout"` + IdleTimeout time.Duration `yaml:"idle_timeout"` + IdleCheckFreq time.Duration `yaml:"idle_check_freq"` + ReadOnly bool `yaml:"read_only"` +} + +// InitDefaults initializing fill config with default values +func (s *Config) InitDefaults() { + s.Addrs = []string{"localhost:6379"} // default addr is pointing to local storage +} diff --git a/plugins/redis/interface.go b/plugins/redis/interface.go new file mode 100644 index 00000000..909c8ca4 --- /dev/null +++ b/plugins/redis/interface.go @@ -0,0 +1,9 @@ +package redis + +import "github.com/go-redis/redis/v8" + +// Redis in the redis KV plugin interface +type Redis interface { + // GetClient + GetClient() redis.UniversalClient +} diff --git a/plugins/redis/plugin.go b/plugins/redis/plugin.go new file mode 100644 index 00000000..fe465340 --- /dev/null +++ b/plugins/redis/plugin.go @@ -0,0 +1,75 @@ +package redis + +import ( + "github.com/go-redis/redis/v8" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +const PluginName = "redis" + +type Plugin struct { + // config for RR integration + cfg *Config + // logger + log logger.Logger + // redis universal client + universalClient redis.UniversalClient +} + +func (s *Plugin) GetClient() redis.UniversalClient { + return s.universalClient +} + +func (s *Plugin) Init(cfg config.Configurer, log logger.Logger) error { + const op = errors.Op("redis plugin init") + s.cfg = &Config{} + s.cfg.InitDefaults() + + err := cfg.UnmarshalKey(PluginName, &s.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + s.log = log + + s.universalClient = redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: s.cfg.Addrs, + DB: s.cfg.DB, + Username: s.cfg.Username, + Password: s.cfg.Password, + SentinelPassword: s.cfg.SentinelPassword, + MaxRetries: s.cfg.MaxRetries, + MinRetryBackoff: s.cfg.MaxRetryBackoff, + MaxRetryBackoff: s.cfg.MaxRetryBackoff, + DialTimeout: s.cfg.DialTimeout, + ReadTimeout: s.cfg.ReadTimeout, + WriteTimeout: s.cfg.WriteTimeout, + PoolSize: s.cfg.PoolSize, + MinIdleConns: s.cfg.MinIdleConns, + MaxConnAge: s.cfg.MaxConnAge, + PoolTimeout: s.cfg.PoolTimeout, + IdleTimeout: s.cfg.IdleTimeout, + IdleCheckFrequency: s.cfg.IdleCheckFreq, + ReadOnly: s.cfg.ReadOnly, + RouteByLatency: s.cfg.RouteByLatency, + RouteRandomly: s.cfg.RouteRandomly, + MasterName: s.cfg.MasterName, + }) + + return nil +} + +func (s *Plugin) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (s Plugin) Stop() error { + return s.universalClient.Close() +} + +func (s *Plugin) Name() string { + return PluginName +} diff --git a/plugins/reload/config.go b/plugins/reload/config.go new file mode 100644 index 00000000..9ca2c0dc --- /dev/null +++ b/plugins/reload/config.go @@ -0,0 +1,58 @@ +package reload + +import ( + "time" + + "github.com/spiral/errors" +) + +// Config is a Reload configuration point. +type Config struct { + // Interval is a global refresh interval + Interval time.Duration + + // Patterns is a global file patterns to watch. It will be applied to every directory in project + Patterns []string + + // Services is set of services which would be reloaded in case of FS changes + Services map[string]ServiceConfig +} + +type ServiceConfig struct { + // Enabled indicates that service must be watched, doest not required when any other option specified + Enabled bool + + // Recursive is options to use nested files from root folder + Recursive bool + + // Patterns is per-service specific files to watch + Patterns []string + + // Dirs is per-service specific dirs which will be combined with Patterns + Dirs []string + + // Ignore is set of files which would not be watched + Ignore []string +} + +// InitDefaults sets missing values to their default values. +func InitDefaults(c *Config) { + c.Interval = time.Second + c.Patterns = []string{".php"} +} + +// Valid validates the configuration. +func (c *Config) Valid() error { + const op = errors.Op("config validation [reload plugin]") + if c.Interval < time.Second { + return errors.E(op, errors.Str("too short interval")) + } + + if c.Services == nil { + return errors.E(op, errors.Str("should add at least 1 service")) + } else if len(c.Services) == 0 { + return errors.E(op, errors.Str("service initialized, however, no config added")) + } + + return nil +} diff --git a/plugins/reload/plugin.go b/plugins/reload/plugin.go new file mode 100644 index 00000000..452e03a3 --- /dev/null +++ b/plugins/reload/plugin.go @@ -0,0 +1,159 @@ +package reload + +import ( + "os" + "strings" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/resetter" +) + +// PluginName contains default plugin name. +const PluginName string = "reload" +const thresholdChanBuffer uint = 1000 + +type Plugin struct { + cfg *Config + log logger.Logger + watcher *Watcher + services map[string]interface{} + res resetter.Resetter + stopc chan struct{} +} + +// Init controller service +func (s *Plugin) Init(cfg config.Configurer, log logger.Logger, res resetter.Resetter) error { + const op = errors.Op("reload plugin init") + s.cfg = &Config{} + InitDefaults(s.cfg) + err := cfg.UnmarshalKey(PluginName, &s.cfg) + if err != nil { + // disable plugin in case of error + return errors.E(op, errors.Disabled, err) + } + + s.log = log + s.res = res + s.stopc = make(chan struct{}, 1) + s.services = make(map[string]interface{}) + + var configs []WatcherConfig + + for serviceName, serviceConfig := range s.cfg.Services { + ignored, err := ConvertIgnored(serviceConfig.Ignore) + if err != nil { + return errors.E(op, err) + } + configs = append(configs, WatcherConfig{ + ServiceName: serviceName, + Recursive: serviceConfig.Recursive, + Directories: serviceConfig.Dirs, + FilterHooks: func(filename string, patterns []string) error { + for i := 0; i < len(patterns); i++ { + if strings.Contains(filename, patterns[i]) { + return nil + } + } + return errors.E(op, errors.Skip) + }, + Files: make(map[string]os.FileInfo), + Ignored: ignored, + FilePatterns: append(serviceConfig.Patterns, s.cfg.Patterns...), + }) + } + + s.watcher, err = NewWatcher(configs, s.log) + if err != nil { + return errors.E(op, err) + } + + return nil +} + +func (s *Plugin) Serve() chan error { + const op = errors.Op("reload plugin serve") + errCh := make(chan error, 1) + if s.cfg.Interval < time.Second { + errCh <- errors.E(op, errors.Str("reload interval is too fast")) + return errCh + } + + // make a map with unique services + // so, if we would have a 100 events from http service + // in map we would see only 1 key and it's config + treshholdc := make(chan struct { + serviceConfig ServiceConfig + service string + }, thresholdChanBuffer) + + // use the same interval + timer := time.NewTimer(s.cfg.Interval) + + go func() { + for e := range s.watcher.Event { + treshholdc <- struct { + serviceConfig ServiceConfig + service string + }{serviceConfig: s.cfg.Services[e.service], service: e.service} + } + }() + + // map with configs by services + updated := make(map[string]ServiceConfig, len(s.cfg.Services)) + + go func() { + for { + select { + case cfg := <-treshholdc: + // logic is following: + // restart + timer.Stop() + // replace previous value in map by more recent without adding new one + updated[cfg.service] = cfg.serviceConfig + // if we getting a lot of events, we shouldn't restart particular service on each of it (user doing batch move or very fast typing) + // instead, we are resetting the timer and wait for s.cfg.Interval time + // If there is no more events, we restart service only once + timer.Reset(s.cfg.Interval) + case <-timer.C: + if len(updated) > 0 { + for name := range updated { + err := s.res.ResetByName(name) + if err != nil { + timer.Stop() + errCh <- errors.E(op, err) + return + } + } + // zero map + updated = make(map[string]ServiceConfig, len(s.cfg.Services)) + } + case <-s.stopc: + timer.Stop() + return + } + } + }() + + go func() { + err := s.watcher.StartPolling(s.cfg.Interval) + if err != nil { + errCh <- errors.E(op, err) + return + } + }() + + return errCh +} + +func (s *Plugin) Stop() error { + s.watcher.Stop() + s.stopc <- struct{}{} + return nil +} + +func (s *Plugin) Name() string { + return PluginName +} diff --git a/plugins/reload/watcher.go b/plugins/reload/watcher.go new file mode 100644 index 00000000..c232f16f --- /dev/null +++ b/plugins/reload/watcher.go @@ -0,0 +1,374 @@ +package reload + +import ( + "io/ioutil" + "os" + "path/filepath" + "sync" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +// SimpleHook is used to filter by simple criteria, CONTAINS +type SimpleHook func(filename string, pattern []string) error + +// An Event describes an event that is received when files or directory +// changes occur. It includes the os.FileInfo of the changed file or +// directory and the type of event that's occurred and the full path of the file. +type Event struct { + Path string + Info os.FileInfo + + service string // type of service, http, grpc, etc... +} + +type WatcherConfig struct { + // service name + ServiceName string + + // Recursive or just add by singe directory + Recursive bool + + // Directories used per-service + Directories []string + + // simple hook, just CONTAINS + FilterHooks func(filename string, pattern []string) error + + // path to file with Files + Files map[string]os.FileInfo + + // Ignored Directories, used map for O(1) amortized get + Ignored map[string]struct{} + + // FilePatterns to ignore + FilePatterns []string +} + +type Watcher struct { + // main event channel + Event chan Event + close chan struct{} + + // ============================= + mu *sync.Mutex + + // indicates is walker started or not + started bool + + // config for each service + // need pointer here to assign files + watcherConfigs map[string]WatcherConfig + + // logger + log logger.Logger +} + +// Options is used to set Watcher Options +type Options func(*Watcher) + +// NewWatcher returns new instance of File Watcher +func NewWatcher(configs []WatcherConfig, log logger.Logger, options ...Options) (*Watcher, error) { + w := &Watcher{ + Event: make(chan Event), + mu: &sync.Mutex{}, + + log: log, + + close: make(chan struct{}), + + //workingDir: workDir, + watcherConfigs: make(map[string]WatcherConfig), + } + + // add watcherConfigs by service names + for _, v := range configs { + w.watcherConfigs[v.ServiceName] = v + } + + // apply options + for _, option := range options { + option(w) + } + err := w.initFs() + if err != nil { + return nil, err + } + + return w, nil +} + +// initFs makes initial map with files +func (w *Watcher) initFs() error { + const op = errors.Op("init fs") + for srvName, config := range w.watcherConfigs { + fileList, err := w.retrieveFileList(srvName, config) + if err != nil { + return errors.E(op, err) + } + // workaround. in golang you can't assign to map in struct field + tmp := w.watcherConfigs[srvName] + tmp.Files = fileList + w.watcherConfigs[srvName] = tmp + } + return nil +} + +// ConvertIgnored is used to convert slice to map with ignored files +func ConvertIgnored(ignored []string) (map[string]struct{}, error) { + if len(ignored) == 0 { + return nil, nil + } + + ign := make(map[string]struct{}, len(ignored)) + for i := 0; i < len(ignored); i++ { + abs, err := filepath.Abs(ignored[i]) + if err != nil { + return nil, err + } + ign[abs] = struct{}{} + } + + return ign, nil +} + +// https://en.wikipedia.org/wiki/Inotify +// SetMaxFileEvents sets max file notify events for Watcher +// In case of file watch errors, this value can be increased system-wide +// For linux: set --> fs.inotify.max_user_watches = 600000 (under /etc/<choose_name_here>.conf) +// Add apply: sudo sysctl -p --system +// func SetMaxFileEvents(events int) Options { +// return func(watcher *Watcher) { +// watcher.maxFileWatchEvents = events +// } +// +// } + +// pass map from outside +func (w *Watcher) retrieveFilesSingle(serviceName, path string) (map[string]os.FileInfo, error) { + const op = errors.Op("retrieve") + stat, err := os.Stat(path) + if err != nil { + return nil, err + } + + filesList := make(map[string]os.FileInfo, 10) + filesList[path] = stat + + // if it's not a dir, return + if !stat.IsDir() { + return filesList, nil + } + + fileInfoList, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + + // recursive calls are slow in compare to goto + // so, we will add files with goto pattern +outer: + for i := 0; i < len(fileInfoList); i++ { + // if file in ignored --> continue + if _, ignored := w.watcherConfigs[serviceName].Ignored[path]; ignored { + continue + } + + // if filename does not contain pattern --> ignore that file + if w.watcherConfigs[serviceName].FilePatterns != nil && w.watcherConfigs[serviceName].FilterHooks != nil { + err = w.watcherConfigs[serviceName].FilterHooks(fileInfoList[i].Name(), w.watcherConfigs[serviceName].FilePatterns) + if errors.Is(errors.Skip, err) { + continue outer + } + } + + filesList[fileInfoList[i].Name()] = fileInfoList[i] + } + + return filesList, nil +} + +func (w *Watcher) StartPolling(duration time.Duration) error { + w.mu.Lock() + const op = errors.Op("start polling") + if w.started { + w.mu.Unlock() + return errors.E(op, errors.Str("already started")) + } + + w.started = true + w.mu.Unlock() + + return w.waitEvent(duration) +} + +// this is blocking operation +func (w *Watcher) waitEvent(d time.Duration) error { + ticker := time.NewTicker(d) + for { + select { + case <-w.close: + ticker.Stop() + // just exit + // no matter for the pollEvents + return nil + case <-ticker.C: + // this is not very effective way + // because we have to wait on Lock + // better is to listen files in parallel, but, since that would be used in debug... TODO + for serviceName := range w.watcherConfigs { + // TODO sync approach + fileList, _ := w.retrieveFileList(serviceName, w.watcherConfigs[serviceName]) + w.pollEvents(w.watcherConfigs[serviceName].ServiceName, fileList) + } + } + } +} + +// retrieveFileList get file list for service +func (w *Watcher) retrieveFileList(serviceName string, config WatcherConfig) (map[string]os.FileInfo, error) { + fileList := make(map[string]os.FileInfo) + if config.Recursive { + // walk through directories recursively + for i := 0; i < len(config.Directories); i++ { + // full path is workdir/relative_path + fullPath, err := filepath.Abs(config.Directories[i]) + if err != nil { + return nil, err + } + list, err := w.retrieveFilesRecursive(serviceName, fullPath) + if err != nil { + return nil, err + } + + for k := range list { + fileList[k] = list[k] + } + } + return fileList, nil + } + + for i := 0; i < len(config.Directories); i++ { + // full path is workdir/relative_path + fullPath, err := filepath.Abs(config.Directories[i]) + if err != nil { + return nil, err + } + + // list is pathToFiles with files + list, err := w.retrieveFilesSingle(serviceName, fullPath) + if err != nil { + return nil, err + } + + for pathToFile, file := range list { + fileList[pathToFile] = file + } + } + + return fileList, nil +} + +func (w *Watcher) retrieveFilesRecursive(serviceName, root string) (map[string]os.FileInfo, error) { + fileList := make(map[string]os.FileInfo) + + return fileList, filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + const op = errors.Op("retrieve files recursive") + if err != nil { + return errors.E(op, err) + } + + // If path is ignored and it's a directory, skip the directory. If it's + // ignored and it's a single file, skip the file. + _, ignored := w.watcherConfigs[serviceName].Ignored[path] + if ignored { + if info.IsDir() { + // if it's dir, ignore whole + return filepath.SkipDir + } + return nil + } + + // if filename does not contain pattern --> ignore that file + err = w.watcherConfigs[serviceName].FilterHooks(info.Name(), w.watcherConfigs[serviceName].FilePatterns) + if errors.Is(errors.Skip, err) { + return nil + } + + // Add the path and it's info to the file list. + fileList[path] = info + return nil + }) +} + +func (w *Watcher) pollEvents(serviceName string, files map[string]os.FileInfo) { + w.mu.Lock() + defer w.mu.Unlock() + + // Store create and remove events for use to check for rename events. + creates := make(map[string]os.FileInfo) + removes := make(map[string]os.FileInfo) + + // Check for removed files. + for pth := range w.watcherConfigs[serviceName].Files { + if _, found := files[pth]; !found { + removes[pth] = w.watcherConfigs[serviceName].Files[pth] + w.log.Debug("file added to the list of removed files", "path", pth, "name", w.watcherConfigs[serviceName].Files[pth].Name(), "size", w.watcherConfigs[serviceName].Files[pth].Size()) + } + } + + // Check for created files, writes and chmods. + for pth := range files { + if files[pth].IsDir() { + continue + } + oldInfo, found := w.watcherConfigs[serviceName].Files[pth] + if !found { + // A file was created. + creates[pth] = files[pth] + w.log.Debug("file was created", "path", pth, "name", files[pth].Name(), "size", files[pth].Size()) + continue + } + + if oldInfo.ModTime() != files[pth].ModTime() || oldInfo.Mode() != files[pth].Mode() { + w.watcherConfigs[serviceName].Files[pth] = files[pth] + w.log.Debug("file was updated", "path", pth, "name", files[pth].Name(), "size", files[pth].Size()) + w.Event <- Event{ + Path: pth, + Info: files[pth], + service: serviceName, + } + } + } + + // Send all the remaining create and remove events. + for pth := range creates { + // add file to the plugin watch files + w.watcherConfigs[serviceName].Files[pth] = creates[pth] + w.log.Debug("file was added to watcher", "path", pth, "name", creates[pth].Name(), "size", creates[pth].Size()) + + w.Event <- Event{ + Path: pth, + Info: creates[pth], + service: serviceName, + } + } + + for pth := range removes { + // delete path from the config + delete(w.watcherConfigs[serviceName].Files, pth) + w.log.Debug("file was removed from watcher", "path", pth, "name", removes[pth].Name(), "size", removes[pth].Size()) + + w.Event <- Event{ + Path: pth, + Info: removes[pth], + service: serviceName, + } + } +} + +func (w *Watcher) Stop() { + w.close <- struct{}{} +} diff --git a/plugins/resetter/interface.go b/plugins/resetter/interface.go new file mode 100644 index 00000000..47d8d791 --- /dev/null +++ b/plugins/resetter/interface.go @@ -0,0 +1,17 @@ +package resetter + +// If plugin implements Resettable interface, than it state can be resetted without reload in runtime via RPC/HTTP +type Resettable interface { + // Reset reload all plugins + Reset() error +} + +// Resetter interface is the Resetter plugin main interface +type Resetter interface { + // Reset all registered plugins + ResetAll() error + // Reset by plugin name + ResetByName(string) error + // GetAll registered plugins + GetAll() []string +} diff --git a/plugins/resetter/plugin.go b/plugins/resetter/plugin.go new file mode 100644 index 00000000..5d294086 --- /dev/null +++ b/plugins/resetter/plugin.go @@ -0,0 +1,80 @@ +package resetter + +import ( + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +const PluginName = "resetter" + +type Plugin struct { + registry map[string]Resettable + log logger.Logger +} + +func (p *Plugin) ResetAll() error { + const op = errors.Op("reset all") + for name := range p.registry { + err := p.registry[name].Reset() + if err != nil { + return errors.E(op, err) + } + } + return nil +} + +func (p *Plugin) ResetByName(plugin string) error { + const op = errors.Op("reset by name") + if plugin, ok := p.registry[plugin]; ok { + return plugin.Reset() + } + return errors.E(op, errors.Errorf("can't find plugin: %s", plugin)) +} + +func (p *Plugin) GetAll() []string { + all := make([]string, 0, len(p.registry)) + for name := range p.registry { + all = append(all, name) + } + return all +} + +func (p *Plugin) Init(log logger.Logger) error { + p.registry = make(map[string]Resettable) + p.log = log + return nil +} + +// Reset named service. +func (p *Plugin) Reset(name string) error { + svc, ok := p.registry[name] + if !ok { + return errors.E("no such service", errors.Str(name)) + } + + return svc.Reset() +} + +// RegisterTarget resettable service. +func (p *Plugin) RegisterTarget(name endure.Named, r Resettable) error { + p.registry[name.Name()] = r + return nil +} + +// Collects declares services to be collected. +func (p *Plugin) Collects() []interface{} { + return []interface{}{ + p.RegisterTarget, + } +} + +// Name of the service. +func (p *Plugin) Name() string { + return PluginName +} + +// RPCService returns associated rpc service. +func (p *Plugin) RPC() interface{} { + return &rpc{srv: p, log: p.log} +} diff --git a/plugins/resetter/rpc.go b/plugins/resetter/rpc.go new file mode 100644 index 00000000..69c955b0 --- /dev/null +++ b/plugins/resetter/rpc.go @@ -0,0 +1,30 @@ +package resetter + +import "github.com/spiral/roadrunner/v2/plugins/logger" + +type rpc struct { + srv *Plugin + log logger.Logger +} + +// List all resettable plugins. +func (rpc *rpc) List(_ bool, list *[]string) error { + rpc.log.Debug("started List method") + *list = make([]string, 0) + + for name := range rpc.srv.registry { + *list = append(*list, name) + } + rpc.log.Debug("services list", "services", *list) + + rpc.log.Debug("finished List method") + return nil +} + +// Reset named plugin. +func (rpc *rpc) Reset(service string, done *bool) error { + rpc.log.Debug("started Reset method for the service", "service", service) + defer rpc.log.Debug("finished Reset method for the service", "service", service) + *done = true + return rpc.srv.Reset(service) +} diff --git a/plugins/rpc/config.go b/plugins/rpc/config.go new file mode 100644 index 00000000..7f3474d7 --- /dev/null +++ b/plugins/rpc/config.go @@ -0,0 +1,47 @@ +package rpc + +import ( + "errors" + "net" + "strings" +) + +// Config defines RPC service config. +type Config struct { + // Listen string + Listen string + + // Disabled disables RPC service. + Disabled bool +} + +// InitDefaults allows to init blank config with pre-defined set of default values. +func (c *Config) InitDefaults() { + if c.Listen == "" { + c.Listen = "tcp://127.0.0.1:6001" + } +} + +// Valid returns nil if config is valid. +func (c *Config) Valid() error { + if dsn := strings.Split(c.Listen, "://"); len(dsn) != 2 { + return errors.New("invalid socket DSN (tcp://:6001, unix://file.sock)") + } + + return nil +} + +// Listener creates new rpc socket Listener. +func (c *Config) Listener() (net.Listener, error) { + return CreateListener(c.Listen) +} + +// Dialer creates rpc socket Dialer. +func (c *Config) Dialer() (net.Conn, error) { + dsn := strings.Split(c.Listen, "://") + if len(dsn) != 2 { + return nil, errors.New("invalid socket DSN (tcp://:6001, unix://file.sock)") + } + + return net.Dial(dsn[0], dsn[1]) +} diff --git a/plugins/rpc/doc/plugin_arch.drawio b/plugins/rpc/doc/plugin_arch.drawio new file mode 100644 index 00000000..dec5f0b2 --- /dev/null +++ b/plugins/rpc/doc/plugin_arch.drawio @@ -0,0 +1 @@ +<mxfile host="Electron" modified="2020-10-19T17:14:19.125Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.7.9 Chrome/85.0.4183.121 Electron/10.1.3 Safari/537.36" etag="2J39x4EyFr1zaE9BXKM4" version="13.7.9" type="device"><diagram id="q2oMKs6VHyn7y0AfAXBL" name="Page-1">7Vttc9o4EP41zLQfksE2GPIxQHPXu7RlQntt7ptiC1sX2XJlOUB//a1sGdtIJDQFnE6YyUys1YutfR7trlai44yj5R8cJeEH5mPasbv+suNMOrZtORcO/JOSVSEZWv1CEHDiq0aVYEZ+YCXsKmlGfJw2GgrGqCBJU+ixOMaeaMgQ52zRbDZntPnWBAVYE8w8RHXpV+KLUEkt96Kq+BOTIFSvHtqDoiJCZWM1kzREPlvURM67jjPmjIniKVqOMZXKK/VS9LvaUrv+MI5jsUuHL/zu0yx7//HT3Pln8vfN59vvS/usVHMqVuWMsQ8KUEXGRcgCFiP6rpKOOMtiH8thu1Cq2lwzloDQAuF/WIiVQhNlgoEoFBFVtXhJxLfa860c6ryvSpOlGjkvrMpCLPjqW71Q6yWLVbe8VPabs1hcoYhQKRizjBPMYcIf8UJVqq+8gGKhC6mArTpWohQG8lSrfz88xF8/ds/+uiLe7MsXtLiyZ2clVxEPsHik3WDNBFhCmEUYvh36cUyRIA/N70CKy8G6XQU3PCjEfwZ9q030K8RvazVPoV8BftvA+7dE33KOBP9jX/mAaKbedDOFkbpTmgUk1qjRBH4REoFnCcr1sADj3wT55xVv0PMD5gIvayJdU6rWGSi3otyMYw3OlWRRme21VwlrFtsdHEi9jqbe9zERha+ak0DTL0xVNJWIKAliePZAMaA+ZyQVQsA5XaqKiPh+sShxSn6gu3woiU7CSCzyCfVHnf5EjgXrMC103go+3Q18hho6QwM4pfPcOzg9DZwJTnDspyBk8Rqk8ylnDxCB8N8DLcveD1z2BlxWWa4vpu4x8epreOmuK/YvZcQnIaAoTYm34XeO5kMMun/aFRjdj45QDYG+AYBStrMHUW+YSgpWBOgNtxCgHKJwgapXPercGKhvbwxkbQxUKEYbKCfJetrP542r8aa0vt0U9gsE1rpzKfWVeK97ia+Xc41glolhB1viA32Jj+3O5YhIXc9loAHFEczdpRKWO95Ay/2eyZ1UrqqzQq8S14tkmeurrIanQP0vRvmVQYA052WwVAwHE7+rXrHBp/bCI3f4tPu1jMGReyCwLT06KoLPVPDMExnHmvrSBYkoinGpIVWz07oUcm8y8kJC/Wu0YpmcXiqQd1+WRiHj5AcMi0qIoJqXMNhuo8VM9lQLO1/oeFqiY22IPqBlo+E1SoUSeIxSlKTkbj2NCGwhiUdMCBbt0/k8P47uuQarULapE8Vye4diytDg+ke7R2hAKHaPx4wyIMYkZgWBCKUbopJDFM/FVgalsOEhcXCdt5n0KsmNUoUUMeg7p3kgEoI/wHG+axZIbPUHI9DyWIYl4BnsMZStqpw7iwT22WMWw1wQycHFwKMFTsUvU+Tx1fk0cUr34e7GE/tQBqV0SxpNpJGeYf6QK+VNjMX5TeK9PbGlTbb07ZbZYl1sYUsKTCEeltvAIlKr+aNuSqHqxJw2mTMwBC7HZY6eOSiYMydYni3IeHH8aILnxIk9c8Lq9tomxQ7pCUpyqAszUZ4lWc/iw3qXqQjwOc+8n1kaSRydJI6BEBTdYTqF3WixH57woq1h0/ryueDsGLAOD0UFPeNQ2AcYPmT+G7FK8NvCTMjHkzdply1HdCfmIzhDHvMIR3Av9jDVrKTOjjnUCzPaRzpN1Ra+Ciafk9Xo/nK6wmAsfpMMhrZ+DazZmsHoNTNdPcvgD1xDpmuwB4dgpIX9dLxY8aTKdZ78wp7osn2t/lQyw8SZg3kFPTmqcSZGkTIsgNeJLS2yxZTMOCpb9IizMigcByQFmyITGlYxV4A2o0iqyc+PvOGvYYPmTNbl2Xgzq17Wgdie/Ia1cYFkqO8pHftAx2FGVPUMVVJkul8VLK61cXJl67gc6pTSbAvcVgJ245259TW5Vm5M1k6i9xPlO7uG+b1Ww3zdOVdXCk5h/pHsgtM0C64p7WNywqWz3j8tdsgLX0tXHJ+itiNFbVsu176UIN/SL7xMOQOFR2lOl7a9fN3MP4rYHpbzxq7dsGk/1O1QMzT6nYOAqSAZFqaPvY78hYecQIBjzJGQgbNgsk2UeaH8Ji93RdLvefdY3ohDeZyNlx7G8iGjJMqvA5/pV61fE9YGy93fU6ANxer3NcWNwupXSs67/wE=</diagram></mxfile>
\ No newline at end of file diff --git a/plugins/rpc/interface.go b/plugins/rpc/interface.go new file mode 100644 index 00000000..683fd2ec --- /dev/null +++ b/plugins/rpc/interface.go @@ -0,0 +1,7 @@ +package rpc + +// RPCer declares the ability to create set of public RPC methods. +type RPCer interface { + // Provides RPC methods for the given service. + RPC() interface{} +} diff --git a/plugins/rpc/plugin.go b/plugins/rpc/plugin.go new file mode 100644 index 00000000..6a83326b --- /dev/null +++ b/plugins/rpc/plugin.go @@ -0,0 +1,164 @@ +package rpc + +import ( + "net" + "net/rpc" + "sync/atomic" + + "github.com/spiral/endure" + "github.com/spiral/errors" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +// PluginName contains default plugin name. +const PluginName = "RPC" + +type pluggable struct { + service RPCer + name string +} + +// Plugin is RPC service. +type Plugin struct { + cfg Config + log logger.Logger + rpc *rpc.Server + services []pluggable + listener net.Listener + closed *uint32 +} + +// Init rpc service. Must return true if service is enabled. +func (s *Plugin) Init(cfg config.Configurer, log logger.Logger) error { + const op = errors.Op("RPC plugin init") + if !cfg.Has(PluginName) { + return errors.E(op, errors.Disabled) + } + + err := cfg.UnmarshalKey(PluginName, &s.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + s.cfg.InitDefaults() + + if s.cfg.Disabled { + return errors.E(op, errors.Disabled) + } + + s.log = log + state := uint32(0) + s.closed = &state + atomic.StoreUint32(s.closed, 0) + + return s.cfg.Valid() +} + +// Serve serves the service. +func (s *Plugin) Serve() chan error { + const op = errors.Op("register service") + errCh := make(chan error, 1) + + s.rpc = rpc.NewServer() + + services := make([]string, 0, len(s.services)) + + // Attach all services + for i := 0; i < len(s.services); i++ { + err := s.Register(s.services[i].name, s.services[i].service.RPC()) + if err != nil { + errCh <- errors.E(op, err) + return errCh + } + + services = append(services, s.services[i].name) + } + + var err error + s.listener, err = s.cfg.Listener() + if err != nil { + errCh <- err + return errCh + } + + s.log.Debug("Started RPC service", "address", s.cfg.Listen, "services", services) + + go func() { + for { + conn, err := s.listener.Accept() + if err != nil { + if atomic.LoadUint32(s.closed) == 1 { + // just log and continue, this is not a critical issue, we just called Stop + s.log.Warn("listener accept error, connection closed", "error", err) + return + } + + s.log.Error("listener accept error", "error", err) + errCh <- errors.E(errors.Op("listener accept"), errors.Serve, err) + return + } + + go s.rpc.ServeCodec(goridgeRpc.NewCodec(conn)) + } + }() + + return errCh +} + +// Stop stops the service. +func (s *Plugin) Stop() error { + // store closed state + atomic.StoreUint32(s.closed, 1) + err := s.listener.Close() + if err != nil { + return errors.E(errors.Op("stop RPC socket"), err) + } + return nil +} + +// Name contains service name. +func (s *Plugin) Name() string { + return PluginName +} + +// Depends declares services to collect for RPC. +func (s *Plugin) Collects() []interface{} { + return []interface{}{ + s.RegisterPlugin, + } +} + +// RegisterPlugin registers RPC service plugin. +func (s *Plugin) RegisterPlugin(name endure.Named, p RPCer) { + s.services = append(s.services, pluggable{ + service: p, + name: name.Name(), + }) +} + +// Register publishes in the server the set of methods of the +// receiver value that satisfy the following conditions: +// - exported method of exported type +// - two arguments, both of exported type +// - the second argument is a pointer +// - one return value, of type error +// It returns an error if the receiver is not an exported type or has +// no suitable methods. It also logs the error using package log. +func (s *Plugin) Register(name string, svc interface{}) error { + if s.rpc == nil { + return errors.E("RPC service is not configured") + } + + return s.rpc.RegisterName(name, svc) +} + +// Client creates new RPC client. +func (s *Plugin) Client() (*rpc.Client, error) { + conn, err := s.cfg.Dialer() + if err != nil { + return nil, err + } + + return rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)), nil +} diff --git a/plugins/rpc/util.go b/plugins/rpc/util.go new file mode 100644 index 00000000..29a475a4 --- /dev/null +++ b/plugins/rpc/util.go @@ -0,0 +1,57 @@ +package rpc + +import ( + "errors" + "fmt" + "net" + "os" + "strings" + "syscall" + + "github.com/valyala/tcplisten" +) + +// CreateListener crates socket listener based on DSN definition. +func CreateListener(address string) (net.Listener, error) { + dsn := strings.Split(address, "://") + if len(dsn) != 2 { + return nil, errors.New("invalid DSN (tcp://:6001, unix://file.sock)") + } + + if dsn[0] != "unix" && dsn[0] != "tcp" { + return nil, errors.New("invalid Protocol (tcp://:6001, unix://file.sock)") + } + + // create unix listener + if dsn[0] == "unix" { + // check if the file exist + if fileExists(dsn[1]) { + err := syscall.Unlink(dsn[1]) + if err != nil { + return nil, fmt.Errorf("error during the unlink syscall: error %v", err) + } + } + return net.Listen(dsn[0], dsn[1]) + } + + // configure and create tcp4 listener + cfg := tcplisten.Config{ + ReusePort: true, + DeferAccept: true, + FastOpen: true, + Backlog: 0, + } + + // only tcp4 is currently supported + return cfg.NewListener("tcp4", dsn[1]) +} + +// fileExists checks if a file exists and is not a directory before we +// try using it to prevent further errors. +func fileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} diff --git a/pkg/plugins/server/config.go b/plugins/server/config.go index 4bef3c5f..4bef3c5f 100644 --- a/pkg/plugins/server/config.go +++ b/plugins/server/config.go diff --git a/pkg/plugins/server/interface.go b/plugins/server/interface.go index 9c1079ea..9c1079ea 100644 --- a/pkg/plugins/server/interface.go +++ b/plugins/server/interface.go diff --git a/pkg/plugins/server/plugin.go b/plugins/server/plugin.go index 3d90c95b..b280d253 100644 --- a/pkg/plugins/server/plugin.go +++ b/plugins/server/plugin.go @@ -8,8 +8,8 @@ import ( "strings" "github.com/spiral/errors" - "github.com/spiral/roadrunner-plugins/config" - "github.com/spiral/roadrunner-plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" // core imports "github.com/spiral/roadrunner/v2/interfaces/events" diff --git a/plugins/static/config.go b/plugins/static/config.go new file mode 100644 index 00000000..f5d26b2d --- /dev/null +++ b/plugins/static/config.go @@ -0,0 +1,76 @@ +package static + +import ( + "os" + "path" + "strings" + + "github.com/spiral/errors" +) + +// Config describes file location and controls access to them. +type Config struct { + Static struct { + // Dir contains name of directory to control access to. + Dir string + + // Forbid specifies list of file extensions which are forbidden for access. + // Example: .php, .exe, .bat, .htaccess and etc. + Forbid []string + + // Always specifies list of extensions which must always be served by static + // service, even if file not found. + Always []string + + // Request headers to add to every static. + Request map[string]string + + // Response headers to add to every static. + Response map[string]string + } +} + +// Valid returns nil if config is valid. +func (c *Config) Valid() error { + const op = errors.Op("static plugin validation") + st, err := os.Stat(c.Static.Dir) + if err != nil { + if os.IsNotExist(err) { + return errors.E(op, errors.Errorf("root directory '%s' does not exists", c.Static.Dir)) + } + + return err + } + + if !st.IsDir() { + return errors.E(op, errors.Errorf("invalid root directory '%s'", c.Static.Dir)) + } + + return nil +} + +// AlwaysForbid must return true if file extension is not allowed for the upload. +func (c *Config) AlwaysForbid(filename string) bool { + ext := strings.ToLower(path.Ext(filename)) + + for _, v := range c.Static.Forbid { + if ext == v { + return true + } + } + + return false +} + +// AlwaysServe must indicate that file is expected to be served by static service. +func (c *Config) AlwaysServe(filename string) bool { + ext := strings.ToLower(path.Ext(filename)) + + for _, v := range c.Static.Always { + if ext == v { + return true + } + } + + return false +} diff --git a/plugins/static/plugin.go b/plugins/static/plugin.go new file mode 100644 index 00000000..06b384df --- /dev/null +++ b/plugins/static/plugin.go @@ -0,0 +1,110 @@ +package static + +import ( + "net/http" + "path" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +// ID contains default service name. +const PluginName = "static" + +const RootPluginName = "http" + +// Plugin serves static files. Potentially convert into middleware? +type Plugin struct { + // server configuration (location, forbidden files and etc) + cfg *Config + + log logger.Logger + + // root is initiated http directory + root http.Dir +} + +// Init must return configure service and return true if service hasStatus enabled. Must return error in case of +// misconfiguration. Services must not be used without proper configuration pushed first. +func (s *Plugin) Init(cfg config.Configurer, log logger.Logger) error { + const op = errors.Op("static plugin init") + err := cfg.UnmarshalKey(RootPluginName, &s.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + s.log = log + s.root = http.Dir(s.cfg.Static.Dir) + + err = s.cfg.Valid() + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + return nil +} + +func (s *Plugin) Name() string { + return PluginName +} + +// middleware must return true if request/response pair is handled within the middleware. +func (s *Plugin) Middleware(next http.Handler) http.HandlerFunc { + // Define the http.HandlerFunc + return func(w http.ResponseWriter, r *http.Request) { + if s.cfg.Static.Request != nil { + for k, v := range s.cfg.Static.Request { + r.Header.Add(k, v) + } + } + + if s.cfg.Static.Response != nil { + for k, v := range s.cfg.Static.Response { + w.Header().Set(k, v) + } + } + + if !s.handleStatic(w, r) { + next.ServeHTTP(w, r) + } + } +} + +func (s *Plugin) handleStatic(w http.ResponseWriter, r *http.Request) bool { + fPath := path.Clean(r.URL.Path) + + if s.cfg.AlwaysForbid(fPath) { + return false + } + + f, err := s.root.Open(fPath) + if err != nil { + s.log.Error("file open error", "error", err) + if s.cfg.AlwaysServe(fPath) { + w.WriteHeader(404) + return true + } + + return false + } + defer func() { + err = f.Close() + if err != nil { + s.log.Error("file closing error", "error", err) + } + }() + + d, err := f.Stat() + if err != nil { + return false + } + + // do not serve directories + if d.IsDir() { + return false + } + + http.ServeContent(w, r, d.Name(), d.ModTime(), f) + return true +} diff --git a/tests/mocks/mock_log.go b/tests/mocks/mock_log.go index 8e3e2836..e9631805 100644 --- a/tests/mocks/mock_log.go +++ b/tests/mocks/mock_log.go @@ -4,7 +4,7 @@ import ( "reflect" "github.com/golang/mock/gomock" - "github.com/spiral/roadrunner-plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/logger" ) // MockLogger is a mock of Logger interface. diff --git a/tests/plugins/checker/configs/.rr-checker-init.yaml b/tests/plugins/checker/configs/.rr-checker-init.yaml new file mode 100755 index 00000000..1273529a --- /dev/null +++ b/tests/plugins/checker/configs/.rr-checker-init.yaml @@ -0,0 +1,31 @@ +rpc: + listen: tcp://127.0.0.1:6005 + disabled: false + +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +status: + address: "127.0.0.1:34333" +logs: + mode: development + level: debug +http: + debug: true + address: 127.0.0.1:11933 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s
\ No newline at end of file diff --git a/tests/plugins/checker/plugin_test.go b/tests/plugins/checker/plugin_test.go new file mode 100644 index 00000000..c346d91a --- /dev/null +++ b/tests/plugins/checker/plugin_test.go @@ -0,0 +1,190 @@ +package checker + +import ( + "io/ioutil" + "net" + "net/http" + "net/rpc" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" + "github.com/spiral/roadrunner/v2/plugins/checker" + "github.com/spiral/roadrunner/v2/plugins/config" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestStatusHttp(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-checker-init.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &checker.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("CheckerGetStatus", checkHTTPStatus) + + stopCh <- struct{}{} + wg.Wait() +} + +const resp = `Service: http: Status: 200 +Service: rpc not found` + +func checkHTTPStatus(t *testing.T) { + req, err := http.NewRequest("GET", "http://127.0.0.1:34333/v1/health?plugin=http&plugin=rpc", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, resp, string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestStatusRPC(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-checker-init.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &checker.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("CheckerGetStatusRpc", checkRPCStatus) + stopCh <- struct{}{} + wg.Wait() +} + +func checkRPCStatus(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6005") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + + st := &checker.Status{} + + err = client.Call("status.Status", "http", &st) + assert.NoError(t, err) + assert.Equal(t, st.Code, 200) +} diff --git a/tests/plugins/config/.rr.yaml b/tests/plugins/config/.rr.yaml new file mode 100755 index 00000000..732a1366 --- /dev/null +++ b/tests/plugins/config/.rr.yaml @@ -0,0 +1,18 @@ +reload: + enabled: true + interval: 1s + patterns: [".php"] + services: + http: + recursive: true + ignore: ["vendor"] + patterns: [".php", ".go",".md",] + dirs: ["."] + jobs: + recursive: false + ignore: ["service/metrics"] + dirs: ["./jobs"] + rpc: + recursive: true + patterns: [".json"] + dirs: [""] diff --git a/tests/plugins/config/config_test.go b/tests/plugins/config/config_test.go new file mode 100755 index 00000000..858fcb80 --- /dev/null +++ b/tests/plugins/config/config_test.go @@ -0,0 +1,66 @@ +package config + +import ( + "os" + "os/signal" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/stretchr/testify/assert" +) + +func TestViperProvider_Init(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + vp := &config.Viper{} + vp.Path = ".rr.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo{}) + if err != nil { + t.Fatal(err) + } + + err = container.Init() + if err != nil { + t.Fatal(err) + } + + errCh, err := container.Serve() + if err != nil { + t.Fatal(err) + } + + // stop by CTRL+C + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + tt := time.NewTicker(time.Second * 2) + + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + assert.NoError(t, container.Stop()) + return + case <-c: + er := container.Stop() + if er != nil { + panic(er) + } + return + case <-tt.C: + tt.Stop() + assert.NoError(t, container.Stop()) + return + } + } +} diff --git a/tests/plugins/config/plugin1.go b/tests/plugins/config/plugin1.go new file mode 100755 index 00000000..2afe79a4 --- /dev/null +++ b/tests/plugins/config/plugin1.go @@ -0,0 +1,53 @@ +package config + +import ( + "errors" + "time" + + "github.com/spiral/roadrunner/v2/plugins/config" +) + +// ReloadConfig is a Reload configuration point. +type ReloadConfig struct { + Interval time.Duration + Patterns []string + Services map[string]ServiceConfig +} + +type ServiceConfig struct { + Enabled bool + Recursive bool + Patterns []string + Dirs []string + Ignore []string +} + +type Foo struct { + configProvider config.Configurer +} + +// Depends on S2 and DB (S3 in the current case) +func (f *Foo) Init(p config.Configurer) error { + f.configProvider = p + return nil +} + +func (f *Foo) Serve() chan error { + errCh := make(chan error, 1) + + r := &ReloadConfig{} + err := f.configProvider.UnmarshalKey("reload", r) + if err != nil { + errCh <- err + } + + if len(r.Patterns) == 0 { + errCh <- errors.New("should be at least one pattern, but got 0") + } + + return errCh +} + +func (f *Foo) Stop() error { + return nil +} diff --git a/tests/plugins/gzip/configs/.rr-http-middlewareNotExist.yaml b/tests/plugins/gzip/configs/.rr-http-middlewareNotExist.yaml new file mode 100644 index 00000000..a2d12706 --- /dev/null +++ b/tests/plugins/gzip/configs/.rr-http-middlewareNotExist.yaml @@ -0,0 +1,25 @@ +server: + command: "php ../../psr-worker.php" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:18103 + maxRequestSize: 1024 + middleware: [ "gzip", "foo" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/gzip/configs/.rr-http-withGzip.yaml b/tests/plugins/gzip/configs/.rr-http-withGzip.yaml new file mode 100644 index 00000000..aff3efdb --- /dev/null +++ b/tests/plugins/gzip/configs/.rr-http-withGzip.yaml @@ -0,0 +1,25 @@ +server: + command: "php ../../psr-worker.php" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:18953 + maxRequestSize: 1024 + middleware: [ "gzip" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/gzip/plugin_test.go b/tests/plugins/gzip/plugin_test.go new file mode 100644 index 00000000..b09d430e --- /dev/null +++ b/tests/plugins/gzip/plugin_test.go @@ -0,0 +1,176 @@ +package gzip + +import ( + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "testing" + + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/gzip" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/spiral/roadrunner/v2/tests/mocks" + "github.com/stretchr/testify/assert" +) + +func TestGzipPlugin(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-withGzip.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("GzipCheckHeader", headerCheck) + + stopCh <- struct{}{} + wg.Wait() +} + +func headerCheck(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:18953", nil) + assert.NoError(t, err) + client := &http.Client{ + Transport: &http.Transport{ + DisableCompression: false, + }, + } + + r, err := client.Do(req) + assert.NoError(t, err) + assert.True(t, r.Uncompressed) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestMiddlewareNotExist(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-middlewareNotExist.yaml", + Prefix: "rr", + } + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Warn("requested middleware does not exist", "requested", "foo").AnyTimes() + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + stopCh <- struct{}{} + wg.Wait() +} diff --git a/tests/plugins/headers/configs/.rr-cors-headers.yaml b/tests/plugins/headers/configs/.rr-cors-headers.yaml new file mode 100644 index 00000000..9d4e8b36 --- /dev/null +++ b/tests/plugins/headers/configs/.rr-cors-headers.yaml @@ -0,0 +1,39 @@ +server: + command: "php ../../http/client.php headers pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:22855 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + cors: + allowedOrigin: "*" + allowedHeaders: "*" + allowedMethods: "GET,POST,PUT,DELETE" + allowCredentials: true + exposedHeaders: "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma" + maxAge: 600 + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + diff --git a/tests/plugins/headers/configs/.rr-headers-init.yaml b/tests/plugins/headers/configs/.rr-headers-init.yaml new file mode 100644 index 00000000..8d63a187 --- /dev/null +++ b/tests/plugins/headers/configs/.rr-headers-init.yaml @@ -0,0 +1,39 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:33453 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + cors: + allowedOrigin: "*" + allowedHeaders: "*" + allowedMethods: "GET,POST,PUT,DELETE" + allowCredentials: true + exposedHeaders: "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma" + maxAge: 600 + request: + "Example-Request-Header": "Value" + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + diff --git a/tests/plugins/headers/configs/.rr-req-headers.yaml b/tests/plugins/headers/configs/.rr-req-headers.yaml new file mode 100644 index 00000000..f8ab9bec --- /dev/null +++ b/tests/plugins/headers/configs/.rr-req-headers.yaml @@ -0,0 +1,32 @@ +server: + command: "php ../../http/client.php header pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:22655 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + diff --git a/tests/plugins/headers/configs/.rr-res-headers.yaml b/tests/plugins/headers/configs/.rr-res-headers.yaml new file mode 100644 index 00000000..36ab4eb3 --- /dev/null +++ b/tests/plugins/headers/configs/.rr-res-headers.yaml @@ -0,0 +1,32 @@ +server: + command: "php ../../http/client.php header pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:22455 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + diff --git a/tests/plugins/headers/headers_plugin_test.go b/tests/plugins/headers/headers_plugin_test.go new file mode 100644 index 00000000..a2ad3357 --- /dev/null +++ b/tests/plugins/headers/headers_plugin_test.go @@ -0,0 +1,367 @@ +package headers + +import ( + "io/ioutil" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/headers" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestHeadersInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-headers-init.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + stopCh <- struct{}{} + wg.Wait() +} + +func TestRequestHeaders(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-req-headers.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("RequestHeaders", reqHeaders) + + stopCh <- struct{}{} + wg.Wait() +} + +func reqHeaders(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:22655?hello=value", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "CUSTOM-HEADER", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestResponseHeaders(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-res-headers.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("ResponseHeaders", resHeaders) + + stopCh <- struct{}{} + wg.Wait() +} + +func resHeaders(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:22455?hello=value", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + assert.Equal(t, "output-header", r.Header.Get("output")) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "CUSTOM-HEADER", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestCORSHeaders(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-cors-headers.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("CORSHeaders", corsHeaders) + t.Run("CORSHeadersPass", corsHeadersPass) + + stopCh <- struct{}{} + wg.Wait() +} + +func corsHeadersPass(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:22855", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) + assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Headers")) + assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Origin")) + assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) + + _, err = ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func corsHeaders(t *testing.T) { + req, err := http.NewRequest("OPTIONS", "http://localhost:22855", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) + assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Headers")) + assert.Equal(t, "GET,POST,PUT,DELETE", r.Header.Get("Access-Control-Allow-Methods")) + assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Origin")) + assert.Equal(t, "600", r.Header.Get("Access-Control-Max-Age")) + assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) + + _, err = ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + err = r.Body.Close() + assert.NoError(t, err) +} diff --git a/tests/plugins/http/attributes_test.go b/tests/plugins/http/attributes_test.go index 54998906..69200a30 100644 --- a/tests/plugins/http/attributes_test.go +++ b/tests/plugins/http/attributes_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/spiral/roadrunner/v2/pkg/plugins/http/attributes" + "github.com/spiral/roadrunner/v2/plugins/http/attributes" "github.com/stretchr/testify/assert" ) diff --git a/tests/plugins/http/handler_test.go b/tests/plugins/http/handler_test.go index 193bf0cf..18558296 100644 --- a/tests/plugins/http/handler_test.go +++ b/tests/plugins/http/handler_test.go @@ -11,8 +11,8 @@ import ( "strings" "github.com/spiral/roadrunner/v2/pkg/pipe" - httpPlugin "github.com/spiral/roadrunner/v2/pkg/plugins/http" poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" "github.com/stretchr/testify/assert" "net/http" diff --git a/tests/plugins/http/http_test.go b/tests/plugins/http/http_plugin_test.go index f4a357f2..88857df5 100644 --- a/tests/plugins/http/http_test.go +++ b/tests/plugins/http/http_plugin_test.go @@ -19,18 +19,18 @@ import ( "github.com/golang/mock/gomock" "github.com/spiral/endure" goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" - "github.com/spiral/roadrunner-plugins/config" - "github.com/spiral/roadrunner-plugins/logger" - "github.com/spiral/roadrunner-plugins/resetter" "github.com/spiral/roadrunner/v2/interfaces/events" - "github.com/spiral/roadrunner/v2/pkg/plugins/informer" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/informer" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/resetter" + "github.com/spiral/roadrunner/v2/plugins/server" "github.com/spiral/roadrunner/v2/tests/mocks" "github.com/spiral/roadrunner/v2/tools" "github.com/yookoala/gofast" - rpcPlugin "github.com/spiral/roadrunner-plugins/rpc" - httpPlugin "github.com/spiral/roadrunner/v2/pkg/plugins/http" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" "github.com/stretchr/testify/assert" ) @@ -74,7 +74,7 @@ func TestHTTPInit(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 5) + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -92,7 +92,7 @@ func TestHTTPInit(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -103,6 +103,7 @@ func TestHTTPInit(t *testing.T) { } }() + stopCh <- struct{}{} wg.Wait() } @@ -140,8 +141,9 @@ func TestHTTPInformerReset(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) + stopCh := make(chan struct{}, 1) + go func() { - tt := time.NewTimer(time.Second * 10) defer wg.Done() for { select { @@ -157,7 +159,7 @@ func TestHTTPInformerReset(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -174,6 +176,8 @@ func TestHTTPInformerReset(t *testing.T) { t.Run("HTTPResetTest", resetTest) t.Run("HTTPEchoTestAfter", echoHTTP) + stopCh <- struct{}{} + wg.Wait() } @@ -258,7 +262,8 @@ func TestSSL(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -276,7 +281,7 @@ func TestSSL(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -291,6 +296,8 @@ func TestSSL(t *testing.T) { t.Run("SSLEcho", sslEcho) t.Run("SSLNoRedirect", sslNoRedirect) t.Run("fCGIecho", fcgiEcho) + + stopCh <- struct{}{} wg.Wait() } @@ -392,7 +399,8 @@ func TestSSLRedirect(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + stopCh := make(chan struct{}, 1) + go func() { defer wg.Done() for { @@ -409,7 +417,7 @@ func TestSSLRedirect(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -422,6 +430,8 @@ func TestSSLRedirect(t *testing.T) { time.Sleep(time.Second * 1) t.Run("SSLRedirect", sslRedirect) + + stopCh <- struct{}{} wg.Wait() } @@ -477,7 +487,8 @@ func TestSSLPushPipes(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + stopCh := make(chan struct{}, 1) + go func() { defer wg.Done() for { @@ -494,7 +505,7 @@ func TestSSLPushPipes(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -507,6 +518,8 @@ func TestSSLPushPipes(t *testing.T) { time.Sleep(time.Second * 1) t.Run("SSLPush", sslPush) + + stopCh <- struct{}{} wg.Wait() } @@ -565,7 +578,7 @@ func TestFastCGI_RequestUri(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -583,7 +596,7 @@ func TestFastCGI_RequestUri(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -596,6 +609,8 @@ func TestFastCGI_RequestUri(t *testing.T) { time.Sleep(time.Second * 1) t.Run("FastCGIServiceRequestUri", fcgiReqURI) + + stopCh <- struct{}{} wg.Wait() } @@ -650,7 +665,7 @@ func TestH2CUpgrade(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -668,7 +683,7 @@ func TestH2CUpgrade(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -681,6 +696,8 @@ func TestH2CUpgrade(t *testing.T) { time.Sleep(time.Second * 1) t.Run("H2cUpgrade", h2cUpgrade) + + stopCh <- struct{}{} wg.Wait() } @@ -739,7 +756,7 @@ func TestH2C(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -757,7 +774,7 @@ func TestH2C(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -770,6 +787,8 @@ func TestH2C(t *testing.T) { time.Sleep(time.Second * 1) t.Run("H2c", h2c) + + stopCh <- struct{}{} wg.Wait() } @@ -829,7 +848,7 @@ func TestHttpMiddleware(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 20) + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -847,7 +866,7 @@ func TestHttpMiddleware(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -860,6 +879,8 @@ func TestHttpMiddleware(t *testing.T) { time.Sleep(time.Second * 1) t.Run("MiddlewareTest", middleware) + + stopCh <- struct{}{} wg.Wait() } @@ -895,7 +916,7 @@ func middleware(t *testing.T) { } func TestHttpEchoErr(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) rIn := ` @@ -971,7 +992,7 @@ logs: wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -989,7 +1010,7 @@ logs: assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -1002,6 +1023,8 @@ logs: time.Sleep(time.Second * 1) t.Run("HttpEchoError", echoError) + + stopCh <- struct{}{} wg.Wait() } @@ -1054,7 +1077,7 @@ func TestHttpEnvVariables(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -1072,7 +1095,7 @@ func TestHttpEnvVariables(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -1085,6 +1108,8 @@ func TestHttpEnvVariables(t *testing.T) { time.Sleep(time.Second * 1) t.Run("EnvVariablesTest", envVarsTest) + + stopCh <- struct{}{} wg.Wait() } diff --git a/tests/plugins/http/parse_test.go b/tests/plugins/http/parse_test.go index df217202..5cc1ce32 100644 --- a/tests/plugins/http/parse_test.go +++ b/tests/plugins/http/parse_test.go @@ -3,7 +3,7 @@ package http import ( "testing" - "github.com/spiral/roadrunner/v2/pkg/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/http" ) var samples = []struct { diff --git a/tests/plugins/http/plugin1.go b/tests/plugins/http/plugin1.go index da887da8..0ec31211 100644 --- a/tests/plugins/http/plugin1.go +++ b/tests/plugins/http/plugin1.go @@ -1,7 +1,7 @@ package http import ( - "github.com/spiral/roadrunner-plugins/config" + "github.com/spiral/roadrunner/v2/plugins/config" ) type Plugin1 struct { diff --git a/tests/plugins/http/plugin_middleware.go b/tests/plugins/http/plugin_middleware.go index 6d67725d..8d02524d 100644 --- a/tests/plugins/http/plugin_middleware.go +++ b/tests/plugins/http/plugin_middleware.go @@ -3,7 +3,7 @@ package http import ( "net/http" - "github.com/spiral/roadrunner-plugins/config" + "github.com/spiral/roadrunner/v2/plugins/config" ) type PluginMiddleware struct { diff --git a/tests/plugins/http/response_test.go b/tests/plugins/http/response_test.go index 229163a2..a9cbf91a 100644 --- a/tests/plugins/http/response_test.go +++ b/tests/plugins/http/response_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/spiral/roadrunner/v2/pkg/payload" - httpPlugin "github.com/spiral/roadrunner/v2/pkg/plugins/http" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" "github.com/stretchr/testify/assert" ) diff --git a/tests/plugins/http/uploads_config_test.go b/tests/plugins/http/uploads_config_test.go index 4a7927c5..e76078ee 100644 --- a/tests/plugins/http/uploads_config_test.go +++ b/tests/plugins/http/uploads_config_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - httpPlugin "github.com/spiral/roadrunner/v2/pkg/plugins/http" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" "github.com/stretchr/testify/assert" ) diff --git a/tests/plugins/http/uploads_test.go b/tests/plugins/http/uploads_test.go index e71ff8c1..7bb25cbf 100644 --- a/tests/plugins/http/uploads_test.go +++ b/tests/plugins/http/uploads_test.go @@ -17,8 +17,8 @@ import ( j "github.com/json-iterator/go" "github.com/spiral/roadrunner/v2/pkg/pipe" - httpPlugin "github.com/spiral/roadrunner/v2/pkg/plugins/http" poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" "github.com/stretchr/testify/assert" ) diff --git a/tests/plugins/informer/.rr-informer.yaml b/tests/plugins/informer/.rr-informer.yaml index d817684f..e50ca9c9 100644 --- a/tests/plugins/informer/.rr-informer.yaml +++ b/tests/plugins/informer/.rr-informer.yaml @@ -1,5 +1,5 @@ server: - command: "php ../../client.php echo pipes" + command: "php ../../http/client.php echo pipes" user: "" group: "" env: diff --git a/tests/plugins/informer/informer_test.go b/tests/plugins/informer/informer_test.go index 15063d7e..d9fc2143 100644 --- a/tests/plugins/informer/informer_test.go +++ b/tests/plugins/informer/informer_test.go @@ -5,17 +5,18 @@ import ( "net/rpc" "os" "os/signal" + "sync" "syscall" "testing" "time" "github.com/spiral/endure" goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" - "github.com/spiral/roadrunner-plugins/config" - "github.com/spiral/roadrunner-plugins/logger" - rpcPlugin "github.com/spiral/roadrunner-plugins/rpc" - "github.com/spiral/roadrunner/v2/pkg/plugins/informer" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/informer" + "github.com/spiral/roadrunner/v2/plugins/logger" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/spiral/roadrunner/v2/plugins/server" "github.com/spiral/roadrunner/v2/tools" "github.com/stretchr/testify/assert" ) @@ -52,33 +53,43 @@ func TestInformerInit(t *testing.T) { sig := make(chan os.Signal, 1) signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - tt := time.NewTimer(time.Second * 15) + stopCh := make(chan struct{}, 1) - t.Run("InformerRpcTest", informerRPCTest) + wg := &sync.WaitGroup{} + wg.Add(1) - for { - select { - case e := <-ch: - assert.Fail(t, "error", e.Error.Error()) - err = cont.Stop() - if err != nil { - assert.FailNow(t, "error", err.Error()) - } - case <-sig: - err = cont.Stop() - if err != nil { - assert.FailNow(t, "error", err.Error()) + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return } - return - case <-tt.C: - // timeout - err = cont.Stop() - if err != nil { - assert.FailNow(t, "error", err.Error()) - } - return } - } + }() + + time.Sleep(time.Second) + t.Run("InformerRpcTest", informerRPCTest) + + stopCh <- struct{}{} + wg.Wait() } func informerRPCTest(t *testing.T) { diff --git a/tests/plugins/informer/test_plugin.go b/tests/plugins/informer/test_plugin.go index 95adfb07..ba281d02 100644 --- a/tests/plugins/informer/test_plugin.go +++ b/tests/plugins/informer/test_plugin.go @@ -4,10 +4,10 @@ import ( "context" "time" - "github.com/spiral/roadrunner-plugins/config" "github.com/spiral/roadrunner/v2/interfaces/worker" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/server" ) var testPoolConfig = poolImpl.Config{ diff --git a/tests/plugins/logger/.rr.yaml b/tests/plugins/logger/.rr.yaml new file mode 100644 index 00000000..cb555ec3 --- /dev/null +++ b/tests/plugins/logger/.rr.yaml @@ -0,0 +1,3 @@ +logs: + mode: development + level: debug
\ No newline at end of file diff --git a/tests/plugins/logger/logger_test.go b/tests/plugins/logger/logger_test.go new file mode 100644 index 00000000..cc788be3 --- /dev/null +++ b/tests/plugins/logger/logger_test.go @@ -0,0 +1,79 @@ +package logger + +import ( + "os" + "os/signal" + "sync" + "testing" + + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/stretchr/testify/assert" +) + +func TestLogger(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = ".rr.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = container.Init() + if err != nil { + t.Fatal(err) + } + + errCh, err := container.Serve() + if err != nil { + t.Fatal(err) + } + + // stop by CTRL+C + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + stopCh := make(chan struct{}, 1) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + assert.NoError(t, container.Stop()) + return + case <-c: + err = container.Stop() + assert.NoError(t, err) + return + case <-stopCh: + assert.NoError(t, container.Stop()) + return + } + } + }() + + stopCh <- struct{}{} + wg.Wait() +} diff --git a/tests/plugins/logger/plugin.go b/tests/plugins/logger/plugin.go new file mode 100644 index 00000000..9ddf9ec9 --- /dev/null +++ b/tests/plugins/logger/plugin.go @@ -0,0 +1,40 @@ +package logger + +import ( + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +type Plugin struct { + config config.Configurer + log logger.Logger +} + +func (p1 *Plugin) Init(cfg config.Configurer, log logger.Logger) error { + p1.config = cfg + p1.log = log + return nil +} + +func (p1 *Plugin) Serve() chan error { + errCh := make(chan error, 1) + p1.log.Error("error", "test", errors.E(errors.Str("test"))) + p1.log.Info("error", "test", errors.E(errors.Str("test"))) + p1.log.Debug("error", "test", errors.E(errors.Str("test"))) + p1.log.Warn("error", "test", errors.E(errors.Str("test"))) + + p1.log.Error("error", "test") + p1.log.Info("error", "test") + p1.log.Debug("error", "test") + p1.log.Warn("error", "test") + return errCh +} + +func (p1 *Plugin) Stop() error { + return nil +} + +func (p1 *Plugin) Name() string { + return "logger_plugin" +} diff --git a/tests/plugins/metrics/.rr-test.yaml b/tests/plugins/metrics/.rr-test.yaml new file mode 100644 index 00000000..37c50395 --- /dev/null +++ b/tests/plugins/metrics/.rr-test.yaml @@ -0,0 +1,16 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +metrics: + # prometheus client address (path /metrics added automatically) + address: localhost:2112 + collect: + app_metric: + type: histogram + help: "Custom application metric" + labels: [ "type" ] + buckets: [ 0.1, 0.2, 0.3, 1.0 ] +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/metrics/docker-compose.yml b/tests/plugins/metrics/docker-compose.yml new file mode 100644 index 00000000..610633b4 --- /dev/null +++ b/tests/plugins/metrics/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3.7' + +services: + prometheus: + image: prom/prometheus + ports: + - 9090:9090 diff --git a/tests/plugins/metrics/metrics_test.go b/tests/plugins/metrics/metrics_test.go new file mode 100644 index 00000000..c94d51bc --- /dev/null +++ b/tests/plugins/metrics/metrics_test.go @@ -0,0 +1,739 @@ +package metrics + +import ( + "io/ioutil" + "net" + "net/http" + "net/rpc" + "os" + "os/signal" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/metrics" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/stretchr/testify/assert" +) + +const dialAddr = "127.0.0.1:6001" +const dialNetwork = "tcp" +const getAddr = "http://localhost:2112/metrics" + +// get request and return body +func get() (string, error) { + r, err := http.Get(getAddr) + if err != nil { + return "", err + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return "", err + } + + err = r.Body.Close() + if err != nil { + return "", err + } + // unsafe + return string(b), err +} + +func TestMetricsInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{} + cfg.Prefix = "rr" + cfg.Path = ".rr-test.yaml" + + err = cont.RegisterAll( + cfg, + &metrics.Plugin{}, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + tt := time.NewTimer(time.Second * 5) + + out, err := get() + assert.NoError(t, err) + + assert.Contains(t, out, "go_gc_duration_seconds") + + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } +} + +func TestMetricsGaugeCollector(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{} + cfg.Prefix = "rr" + cfg.Path = ".rr-test.yaml" + + err = cont.RegisterAll( + cfg, + &metrics.Plugin{}, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + time.Sleep(time.Second) + tt := time.NewTimer(time.Second * 5) + + out, err := get() + assert.NoError(t, err) + assert.Contains(t, out, "my_gauge 100") + assert.Contains(t, out, "my_gauge2 100") + + out, err = get() + assert.NoError(t, err) + assert.Contains(t, out, "go_gc_duration_seconds") + + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } +} + +func TestMetricsDifferentRPCCalls(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{} + cfg.Prefix = "rr" + cfg.Path = ".rr-test.yaml" + + err = cont.RegisterAll( + cfg, + &metrics.Plugin{}, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + go func() { + tt := time.NewTimer(time.Minute * 3) + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("DeclareMetric", declareMetricsTest) + genericOut, err := get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "test_metrics_named_collector") + + t.Run("AddMetric", addMetricsTest) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "test_metrics_named_collector 10000") + + t.Run("SetMetric", setMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "user_gauge_collector 100") + + t.Run("VectorMetric", vectorMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "gauge_2_collector{section=\"first\",type=\"core\"} 100") + + t.Run("MissingSection", missingSection) + t.Run("SetWithoutLabels", setWithoutLabels) + t.Run("SetOnHistogram", setOnHistogram) + t.Run("MetricSub", subMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "sub_gauge_subMetric 1") + + t.Run("SubVector", subVector) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "sub_gauge_subVector{section=\"first\",type=\"core\"} 1") + + t.Run("RegisterHistogram", registerHistogram) + + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, `TYPE histogram_registerHistogram`) + + // check buckets + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="0.1"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="0.2"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="0.5"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="+Inf"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_sum 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_count 0`) + + t.Run("CounterMetric", counterMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "HELP default_default_counter_CounterMetric test_counter") + assert.Contains(t, genericOut, `default_default_counter_CounterMetric{section="section2",type="type2"}`) + + t.Run("ObserveMetric", observeMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "observe_observeMetric") + + t.Run("ObserveMetricNotEnoughLabels", observeMetricNotEnoughLabels) + + close(sig) +} + +func observeMetricNotEnoughLabels(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "observe_observeMetricNotEnoughLabels", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Help: "test_observe", + Type: metrics.Histogram, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + assert.Error(t, client.Call("metrics.Observe", metrics.Metric{ + Name: "observe_observeMetric", + Value: 100.0, + Labels: []string{"test"}, + }, &ret)) + assert.False(t, ret) +} + +func observeMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "observe_observeMetric", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Help: "test_observe", + Type: metrics.Histogram, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + assert.NoError(t, client.Call("metrics.Observe", metrics.Metric{ + Name: "observe_observeMetric", + Value: 100.0, + Labels: []string{"test", "test2"}, + }, &ret)) + assert.True(t, ret) +} + +func counterMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "counter_CounterMetric", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Help: "test_counter", + Type: metrics.Counter, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + assert.NoError(t, client.Call("metrics.Add", metrics.Metric{ + Name: "counter_CounterMetric", + Value: 100.0, + Labels: []string{"type2", "section2"}, + }, &ret)) + assert.True(t, ret) +} + +func registerHistogram(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "histogram_registerHistogram", + Collector: metrics.Collector{ + Help: "test_histogram", + Type: metrics.Histogram, + Buckets: []float64{0.1, 0.2, 0.5}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "histogram_registerHistogram", + Value: 10000, + Labels: nil, + } + + err = client.Call("metrics.Add", m, &ret) + assert.Error(t, err) + assert.False(t, ret) +} + +func subVector(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "sub_gauge_subVector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m := metrics.Metric{ + Name: "sub_gauge_subVector", + Value: 100000, + Labels: []string{"core", "first"}, + } + + err = client.Call("metrics.Add", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m = metrics.Metric{ + Name: "sub_gauge_subVector", + Value: 99999, + Labels: []string{"core", "first"}, + } + + err = client.Call("metrics.Sub", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func subMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "sub_gauge_subMetric", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m := metrics.Metric{ + Name: "sub_gauge_subMetric", + Value: 100000, + } + + err = client.Call("metrics.Add", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m = metrics.Metric{ + Name: "sub_gauge_subMetric", + Value: 99999, + } + + err = client.Call("metrics.Sub", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func setOnHistogram(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "histogram_setOnHistogram", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Histogram, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_setOnHistogram", + Value: 100.0, + } + + err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"} + assert.Error(t, err) + assert.False(t, ret) +} + +func setWithoutLabels(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "gauge_setWithoutLabels", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_setWithoutLabels", + Value: 100.0, + } + + err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"} + assert.Error(t, err) + assert.False(t, ret) +} + +func missingSection(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "gauge_missing_section_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_missing_section_collector", + Value: 100.0, + Labels: []string{"missing"}, + } + + err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"} + assert.Error(t, err) + assert.False(t, ret) +} + +func vectorMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "gauge_2_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_2_collector", + Value: 100.0, + Labels: []string{"core", "first"}, + } + + err = client.Call("metrics.Set", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func setMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "user_gauge_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m := metrics.Metric{ + Name: "user_gauge_collector", + Value: 100.0, + } + + err = client.Call("metrics.Set", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func addMetricsTest(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + m := metrics.Metric{ + Name: "test_metrics_named_collector", + Value: 10000, + Labels: nil, + } + + err = client.Call("metrics.Add", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func declareMetricsTest(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "test_metrics_named_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Counter, + Help: "NO HELP!", + Labels: nil, + Buckets: nil, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} diff --git a/tests/plugins/metrics/plugin1.go b/tests/plugins/metrics/plugin1.go new file mode 100644 index 00000000..ae024a8a --- /dev/null +++ b/tests/plugins/metrics/plugin1.go @@ -0,0 +1,46 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/spiral/roadrunner/v2/plugins/config" +) + +// Gauge ////////////// +type Plugin1 struct { + config config.Configurer +} + +func (p1 *Plugin1) Init(cfg config.Configurer) error { + p1.config = cfg + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "metrics_test.plugin1" +} + +func (p1 *Plugin1) MetricsCollector() []prometheus.Collector { + collector := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "my_gauge", + Help: "My gauge value", + }) + + collector.Set(100) + + collector2 := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "my_gauge2", + Help: "My gauge2 value", + }) + + collector2.Set(100) + return []prometheus.Collector{collector, collector2} +} diff --git a/tests/plugins/mocks/mock_log.go b/tests/plugins/mocks/mock_log.go new file mode 100644 index 00000000..e9631805 --- /dev/null +++ b/tests/plugins/mocks/mock_log.go @@ -0,0 +1,150 @@ +package mocks + +import ( + "reflect" + + "github.com/golang/mock/gomock" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +// MockLogger is a mock of Logger interface. +type MockLogger struct { + ctrl *gomock.Controller + recorder *MockLoggerMockRecorder +} + +// MockLoggerMockRecorder is the mock recorder for MockLogger. +type MockLoggerMockRecorder struct { + mock *MockLogger +} + +// NewMockLogger creates a new mock instance. +func NewMockLogger(ctrl *gomock.Controller) *MockLogger { + mock := &MockLogger{ctrl: ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { + return m.recorder +} + +func (m *MockLogger) Init() error { + mock := &MockLogger{ctrl: m.ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return nil +} + +// Debug mocks base method. +func (m *MockLogger) Debug(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debug", varargs...) +} + +// Warn mocks base method. +func (m *MockLogger) Warn(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Warn", varargs...) +} + +// Info mocks base method. +func (m *MockLogger) Info(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Info", varargs...) +} + +// Error mocks base method. +func (m *MockLogger) Error(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Error", varargs...) +} + +// Warn indicates an expected call of Warn. +func (mr *MockLoggerMockRecorder) Warn(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), varargs...) +} + +// Debug indicates an expected call of Debug. +func (mr *MockLoggerMockRecorder) Debug(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), varargs...) +} + +// Error indicates an expected call of Error. +func (mr *MockLoggerMockRecorder) Error(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), varargs...) +} + +func (mr *MockLoggerMockRecorder) Init() error { + return nil +} + +// Info indicates an expected call of Info. +func (mr *MockLoggerMockRecorder) Info(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), varargs...) +} + +// MockWithLogger is a mock of WithLogger interface. +type MockWithLogger struct { + ctrl *gomock.Controller + recorder *MockWithLoggerMockRecorder +} + +// MockWithLoggerMockRecorder is the mock recorder for MockWithLogger. +type MockWithLoggerMockRecorder struct { + mock *MockWithLogger +} + +// NewMockWithLogger creates a new mock instance. +func NewMockWithLogger(ctrl *gomock.Controller) *MockWithLogger { + mock := &MockWithLogger{ctrl: ctrl} + mock.recorder = &MockWithLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockWithLogger) EXPECT() *MockWithLoggerMockRecorder { + return m.recorder +} + +// With mocks base method. +func (m *MockWithLogger) With(keyvals ...interface{}) logger.Logger { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range keyvals { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "With", varargs...) + ret0, _ := ret[0].(logger.Logger) + return ret0 +} + +// With indicates an expected call of With. +func (mr *MockWithLoggerMockRecorder) With(keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "With", reflect.TypeOf((*MockWithLogger)(nil).With), keyvals...) +} diff --git a/tests/plugins/redis/plugin1.go b/tests/plugins/redis/plugin1.go new file mode 100644 index 00000000..e50213e5 --- /dev/null +++ b/tests/plugins/redis/plugin1.go @@ -0,0 +1,43 @@ +package redis + +import ( + "context" + "time" + + "github.com/go-redis/redis/v8" + "github.com/spiral/errors" + redisPlugin "github.com/spiral/roadrunner/v2/plugins/redis" +) + +type Plugin1 struct { + redisClient redis.UniversalClient +} + +func (p *Plugin1) Init(redis redisPlugin.Redis) error { + p.redisClient = redis.GetClient() + return nil +} + +func (p *Plugin1) Serve() chan error { + const op = errors.Op("plugin1 serve") + errCh := make(chan error, 1) + p.redisClient.Set(context.Background(), "foo", "bar", time.Minute) + + stringCmd := p.redisClient.Get(context.Background(), "foo") + data, err := stringCmd.Result() + if err != nil { + errCh <- errors.E(op, err) + return errCh + } + + if data != "bar" { + errCh <- errors.E(op, errors.Str("no such key")) + return errCh + } + + return errCh +} + +func (p *Plugin1) Stop() error { + return nil +} diff --git a/tests/plugins/redis/redis_plugin_test.go b/tests/plugins/redis/redis_plugin_test.go new file mode 100644 index 00000000..eba05752 --- /dev/null +++ b/tests/plugins/redis/redis_plugin_test.go @@ -0,0 +1,120 @@ +package redis + +import ( + "fmt" + "os" + "os/signal" + "sync" + "syscall" + "testing" + + "github.com/alicebob/miniredis/v2" + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/redis" + "github.com/spiral/roadrunner/v2/tests/mocks" + "github.com/stretchr/testify/assert" +) + +func redisConfig(port string) string { + cfg := ` +redis: + addrs: + - 'localhost:%s' + master_name: '' + username: '' + password: '' + db: 0 + sentinel_password: '' + route_by_latency: false + route_randomly: false + dial_timeout: 0 + max_retries: 1 + min_retry_backoff: 0 + max_retry_backoff: 0 + pool_size: 0 + min_idle_conns: 0 + max_conn_age: 0 + read_timeout: 0 + write_timeout: 0 + pool_timeout: 0 + idle_timeout: 0 + idle_check_freq: 0 + read_only: false +` + return fmt.Sprintf(cfg, port) +} + +func TestRedisInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + s, err := miniredis.Run() + assert.NoError(t, err) + + c := redisConfig(s.Port()) + + cfg := &config.Viper{} + cfg.Type = "yaml" + cfg.ReadInCfg = []byte(c) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + err = cont.RegisterAll( + cfg, + mockLogger, + &redis.Plugin{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + stopCh <- struct{}{} + wg.Wait() +} diff --git a/tests/plugins/reload/config_test.go b/tests/plugins/reload/config_test.go new file mode 100644 index 00000000..72c11070 --- /dev/null +++ b/tests/plugins/reload/config_test.go @@ -0,0 +1,63 @@ +package reload + +import ( + "testing" + "time" + + "github.com/spiral/roadrunner/v2/plugins/reload" + "github.com/stretchr/testify/assert" +) + +func Test_Config_Valid(t *testing.T) { + services := make(map[string]reload.ServiceConfig) + services["test"] = reload.ServiceConfig{ + Recursive: false, + Patterns: nil, + Dirs: nil, + Ignore: nil, + } + + cfg := &reload.Config{ + Interval: time.Second, + Patterns: nil, + Services: services, + } + assert.NoError(t, cfg.Valid()) +} + +func Test_Fake_ServiceConfig(t *testing.T) { + services := make(map[string]reload.ServiceConfig) + cfg := &reload.Config{ + Interval: time.Microsecond, + Patterns: nil, + Services: services, + } + assert.Error(t, cfg.Valid()) +} + +func Test_Interval(t *testing.T) { + services := make(map[string]reload.ServiceConfig) + services["test"] = reload.ServiceConfig{ + Enabled: false, + Recursive: false, + Patterns: nil, + Dirs: nil, + Ignore: nil, + } + + cfg := &reload.Config{ + Interval: time.Millisecond, // should crash here + Patterns: nil, + Services: services, + } + assert.Error(t, cfg.Valid()) +} + +func Test_NoServiceConfig(t *testing.T) { + cfg := &reload.Config{ + Interval: time.Second, + Patterns: nil, + Services: nil, + } + assert.Error(t, cfg.Valid()) +} diff --git a/tests/plugins/reload/configs/.rr-reload-2.yaml b/tests/plugins/reload/configs/.rr-reload-2.yaml new file mode 100644 index 00000000..ab32b2d1 --- /dev/null +++ b/tests/plugins/reload/configs/.rr-reload-2.yaml @@ -0,0 +1,44 @@ +server: + command: php ../../psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:27388' + maxRequestSize: 1024 + middleware: + - '' + uploads: + forbid: + - .php + - .exe + - .bat + trustedSubnets: + - 10.0.0.0/8 + - 127.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - '::1/128' + - 'fc00::/7' + - 'fe80::/10' + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error +reload: + interval: 2s + patterns: + - .txt + services: + http: + dirs: + - './unit_tests' + recursive: true diff --git a/tests/plugins/reload/configs/.rr-reload-3.yaml b/tests/plugins/reload/configs/.rr-reload-3.yaml new file mode 100644 index 00000000..881d9b88 --- /dev/null +++ b/tests/plugins/reload/configs/.rr-reload-3.yaml @@ -0,0 +1,46 @@ +server: + command: php ../../psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:37388' + maxRequestSize: 1024 + middleware: + - '' + uploads: + forbid: + - .php + - .exe + - .bat + trustedSubnets: + - 10.0.0.0/8 + - 127.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - '::1/128' + - 'fc00::/7' + - 'fe80::/10' + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error +reload: + interval: 2s + patterns: + - .txt + services: + http: + dirs: + - './unit_tests' + - './unit_tests_copied' + - './dir1' + recursive: true diff --git a/tests/plugins/reload/configs/.rr-reload-4.yaml b/tests/plugins/reload/configs/.rr-reload-4.yaml new file mode 100644 index 00000000..d47df558 --- /dev/null +++ b/tests/plugins/reload/configs/.rr-reload-4.yaml @@ -0,0 +1,46 @@ +server: + command: php ../../psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:22766' + maxRequestSize: 1024 + middleware: + - '' + uploads: + forbid: + - .php + - .exe + - .bat + trustedSubnets: + - 10.0.0.0/8 + - 127.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - '::1/128' + - 'fc00::/7' + - 'fe80::/10' + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error +reload: + interval: 2s + patterns: + - .aaa + services: + http: + dirs: + - './unit_tests' + - './unit_tests_copied' + - './dir1' + recursive: false diff --git a/tests/plugins/reload/configs/.rr-reload.yaml b/tests/plugins/reload/configs/.rr-reload.yaml new file mode 100644 index 00000000..794c41f2 --- /dev/null +++ b/tests/plugins/reload/configs/.rr-reload.yaml @@ -0,0 +1,44 @@ +server: + command: php ../../psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:22388' + maxRequestSize: 1024 + middleware: + - '' + uploads: + forbid: + - .php + - .exe + - .bat + trustedSubnets: + - 10.0.0.0/8 + - 127.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - '::1/128' + - 'fc00::/7' + - 'fe80::/10' + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error +reload: + interval: 1s + patterns: + - .txt + services: + http: + dirs: + - './unit_tests' + recursive: true diff --git a/tests/plugins/reload/reload_plugin_test.go b/tests/plugins/reload/reload_plugin_test.go new file mode 100644 index 00000000..c83d4787 --- /dev/null +++ b/tests/plugins/reload/reload_plugin_test.go @@ -0,0 +1,827 @@ +package reload + +import ( + "io" + "io/ioutil" + "math/rand" + "os" + "os/signal" + "path/filepath" + "strconv" + "sync" + "syscall" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/reload" + "github.com/spiral/roadrunner/v2/plugins/resetter" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/spiral/roadrunner/v2/tests/mocks" + "github.com/stretchr/testify/assert" +) + +const testDir string = "unit_tests" +const testCopyToDir string = "unit_tests_copied" +const dir1 string = "dir1" +const hugeNumberOfFiles uint = 500 + +func TestReloadInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + err = os.Mkdir(testDir, 0755) + assert.NoError(t, err) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1) + mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", "file.txt", "size", gomock.Any()).Times(2) + mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", "file.txt", "size", gomock.Any()).Times(2) + mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").Times(1) + mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").Times(1) + mockLogger.EXPECT().Info("HTTP listeners successfully re-added").Times(1) + mockLogger.EXPECT().Info("HTTP plugin successfully restarted").Times(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("ReloadTestInit", reloadTestInit) + + stopCh <- struct{}{} + wg.Wait() + assert.NoError(t, freeResources(testDir)) +} + +func reloadTestInit(t *testing.T) { + err := ioutil.WriteFile(filepath.Join(testDir, "file.txt"), //nolint:gosec + []byte{}, 0755) + assert.NoError(t, err) +} + +func TestReloadHugeNumberOfFiles(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + + assert.NoError(t, os.Mkdir(testDir, 0755)) + assert.NoError(t, os.Mkdir(testCopyToDir, 0755)) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1) + mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("file was updated", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").MinTimes(1) + mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").MinTimes(1) + mockLogger.EXPECT().Info("HTTP listeners successfully re-added").MinTimes(1) + mockLogger.EXPECT().Info("HTTP plugin successfully restarted").MinTimes(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("ReloadTestHugeNumberOfFiles", reloadHugeNumberOfFiles) + t.Run("ReloadRandomlyChangeFile", randomlyChangeFile) + + stopCh <- struct{}{} + wg.Wait() + + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) +} + +func randomlyChangeFile(t *testing.T) { + // we know, that directory contains 500 files (0-499) + // let's try to randomly change it + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(500) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec + err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".txt"), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } +} + +func reloadHugeNumberOfFiles(t *testing.T) { + for i := uint(0); i < hugeNumberOfFiles; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } +} + +// Should be events only about creating files with txt ext +func TestReloadFilterFileExt(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload-2.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, os.Mkdir(testDir, 0755)) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1) + mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(100) + mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").Times(1) + mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").Times(1) + mockLogger.EXPECT().Info("HTTP listeners successfully re-added").Times(1) + mockLogger.EXPECT().Info("HTTP plugin successfully restarted").Times(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("ReloadMakeFiles", reloadMakeFiles) + t.Run("ReloadFilteredExt", reloadFilteredExt) + + stopCh <- struct{}{} + wg.Wait() + + assert.NoError(t, freeResources(testDir)) +} + +func reloadMakeFiles(t *testing.T) { + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".abc")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".def")) + } +} + +func reloadFilteredExt(t *testing.T) { + // change files with abc extension + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(1000) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec + err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".abc"), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } + + // change files with def extension + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(1000) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec + err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".def"), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } +} + +// Should be events only about creating files with txt ext +func TestReloadCopy500(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload-3.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources(dir1)) + + assert.NoError(t, os.Mkdir(testDir, 0755)) + assert.NoError(t, os.Mkdir(testCopyToDir, 0755)) + assert.NoError(t, os.Mkdir(dir1, 0755)) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + // + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1) + mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50) + mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50) + mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50) + mockLogger.EXPECT().Debug("file was removed from watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50) + mockLogger.EXPECT().Debug("file was updated", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50) + mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").MinTimes(1) + mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").MinTimes(1) + mockLogger.EXPECT().Info("HTTP listeners successfully re-added").MinTimes(1) + mockLogger.EXPECT().Info("HTTP plugin successfully restarted").MinTimes(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + // Scenario + // 1 + // Create 3k files with txt, abc, def extensions + // Copy files to the unit_tests_copy dir + // 2 + // Delete both dirs, recreate + // Create 3k files with txt, abc, def extensions + // Move files to the unit_tests_copy dir + // 3 + // Recursive + + t.Run("ReloadMake300Files", reloadMake300Files) + t.Run("ReloadCopyFiles", reloadCopyFiles) + t.Run("ReloadRecursiveDirsSupport", copyFilesRecursive) + t.Run("RandomChangesInRecursiveDirs", randomChangesInRecursiveDirs) + t.Run("RemoveFilesSupport", removeFilesSupport) + t.Run("ReloadMoveSupport", reloadMoveSupport) + + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources(dir1)) + + stopCh <- struct{}{} + wg.Wait() +} + +func reloadMoveSupport(t *testing.T) { + t.Run("MoveSupportCopy", copyFilesRecursive) + // move some files + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(500) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(100)) // nolint:gosec + rDir := rand.Int63n(9) // nolint:gosec + rExt := rand.Int63n(3) // nolint:gosec + + ext := []string{ + ".txt", + ".abc", + ".def", + } + + // change files with def extension + dirs := []string{ + "dir1", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/dir4", + "dir1/dir2/dir3/dir4/dir5", + "dir1/dir2/dir3/dir4/dir5/dir6", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10", + } + + // move file + err := os.Rename(filepath.Join(dirs[rDir], "file_"+strconv.Itoa(int(rNum))+ext[rExt]), filepath.Join(dirs[rDir+1], "file_"+strconv.Itoa(int(rNum))+ext[rExt])) + assert.NoError(t, err) + } +} + +func removeFilesSupport(t *testing.T) { + // remove some files + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(500) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(100)) // nolint:gosec + rDir := rand.Int63n(10) // nolint:gosec + rExt := rand.Int63n(3) // nolint:gosec + + ext := []string{ + ".txt", + ".abc", + ".def", + } + + // change files with def extension + dirs := []string{ + "dir1", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/dir4", + "dir1/dir2/dir3/dir4/dir5", + "dir1/dir2/dir3/dir4/dir5/dir6", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10", + } + // here can be a situation, when file already deleted + _ = os.Remove(filepath.Join(dirs[rDir], "file_"+strconv.Itoa(int(rNum))+ext[rExt])) + } +} + +func randomChangesInRecursiveDirs(t *testing.T) { + // change files with def extension + dirs := []string{ + "dir1", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/dir4", + "dir1/dir2/dir3/dir4/dir5", + "dir1/dir2/dir3/dir4/dir5/dir6", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10", + } + + ext := []string{ + ".txt", + ".abc", + ".def", + } + + filenames := []string{ + "file_", // should be update + "foo_", // should be created + "bar_", // should be created + } + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(500) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(100)) // nolint:gosec + rDir := rand.Int63n(10) // nolint:gosec + rExt := rand.Int63n(3) // nolint:gosec + rName := rand.Int63n(3) // nolint:gosec + + err := ioutil.WriteFile(filepath.Join(dirs[rDir], filenames[rName]+strconv.Itoa(int(rNum))+ext[rExt]), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } +} + +func copyFilesRecursive(t *testing.T) { + err := copyDir(testDir, "dir1") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10") + assert.NoError(t, err) +} + +func reloadCopyFiles(t *testing.T) { + err := copyDir(testDir, testCopyToDir) + assert.NoError(t, err) + + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + + assert.NoError(t, os.Mkdir(testDir, 0755)) + assert.NoError(t, os.Mkdir(testCopyToDir, 0755)) + + // recreate files + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".abc")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".def")) + } + + err = copyDir(testDir, testCopyToDir) + assert.NoError(t, err) +} + +func reloadMake300Files(t *testing.T) { + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".abc")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".def")) + } +} + +func TestReloadNoRecursion(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload-4.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources(dir1)) + + assert.NoError(t, os.Mkdir(testDir, 0755)) + assert.NoError(t, os.Mkdir(dir1, 0755)) + assert.NoError(t, os.Mkdir(testCopyToDir, 0755)) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + // http server should not be restarted. all event from wrong file extensions should be skipped + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1) + mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("ReloadMakeFiles", reloadMakeFiles) // make files in the testDir + t.Run("ReloadCopyFilesRecursive", reloadCopyFiles) + + stopCh <- struct{}{} + wg.Wait() + + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources(dir1)) +} + +// ======================================================================== + +func freeResources(path string) error { + return os.RemoveAll(path) +} + +func makeFile(filename string) error { + return ioutil.WriteFile(filepath.Join(testDir, filename), []byte{}, 0755) //nolint:gosec +} + +func copyDir(src string, dst string) error { + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + si, err := os.Stat(src) + if err != nil { + return err + } + if !si.IsDir() { + return errors.E(errors.Str("source is not a directory")) + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return err + } + + err = os.MkdirAll(dst, si.Mode()) + if err != nil { + return err + } + + entries, err := ioutil.ReadDir(src) + if err != nil { + return err + } + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + err = copyDir(srcPath, dstPath) + if err != nil { + return err + } + } else { + // Skip symlinks. + if entry.Mode()&os.ModeSymlink != 0 { + continue + } + + err = copyFile(srcPath, dstPath) + if err != nil { + return err + } + } + } + return nil +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return errors.E(err) + } + defer func() { + _ = in.Close() + }() + + out, err := os.Create(dst) + if err != nil { + return errors.E(err) + } + defer func() { + _ = out.Close() + }() + + _, err = io.Copy(out, in) + if err != nil { + return errors.E(err) + } + + err = out.Sync() + if err != nil { + return errors.E(err) + } + + si, err := os.Stat(src) + if err != nil { + return errors.E(err) + } + err = os.Chmod(dst, si.Mode()) + if err != nil { + return errors.E(err) + } + return nil +} diff --git a/tests/plugins/resetter/.rr-resetter.yaml b/tests/plugins/resetter/.rr-resetter.yaml new file mode 100644 index 00000000..e50ca9c9 --- /dev/null +++ b/tests/plugins/resetter/.rr-resetter.yaml @@ -0,0 +1,16 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "pipes" + relayTimeout: "20s" + +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/resetter/resetter_test.go b/tests/plugins/resetter/resetter_test.go new file mode 100644 index 00000000..89dd43c7 --- /dev/null +++ b/tests/plugins/resetter/resetter_test.go @@ -0,0 +1,113 @@ +package resetter + +import ( + "net" + "net/rpc" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/resetter" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestResetterInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{ + Path: ".rr-resetter.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &server.Plugin{}, + &logger.ZapLogger{}, + &resetter.Plugin{}, + &rpcPlugin.Plugin{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + stopCh := make(chan struct{}, 1) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + + t.Run("ResetterRpcTest", resetterRPCTest) + stopCh <- struct{}{} + wg.Wait() +} + +func resetterRPCTest(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6001") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + // WorkerList contains list of workers. + + var ret bool + err = client.Call("resetter.Reset", "resetter.plugin1", &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + var services []string + err = client.Call("resetter.List", nil, &services) + assert.NotNil(t, services) + assert.NoError(t, err) + if services[0] != "resetter.plugin1" { + t.Fatal("no enough services") + } +} diff --git a/tests/plugins/resetter/test_plugin.go b/tests/plugins/resetter/test_plugin.go new file mode 100644 index 00000000..7d53bca0 --- /dev/null +++ b/tests/plugins/resetter/test_plugin.go @@ -0,0 +1,66 @@ +package resetter + +import ( + "context" + "time" + + poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/server" +) + +var testPoolConfig = poolImpl.Config{ + NumWorkers: 10, + MaxJobs: 100, + AllocateTimeout: time.Second * 10, + DestroyTimeout: time.Second * 10, + Supervisor: &poolImpl.SupervisorConfig{ + WatchTick: 60, + TTL: 1000, + IdleTTL: 10, + ExecTTL: 10, + MaxWorkerMemory: 1000, + }, +} + +// Gauge ////////////// +type Plugin1 struct { + config config.Configurer + server server.Server +} + +func (p1 *Plugin1) Init(cfg config.Configurer, server server.Server) error { + p1.config = cfg + p1.server = server + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "resetter.plugin1" +} + +func (p1 *Plugin1) Reset() error { + pool, err := p1.server.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + panic(err) + } + pool.Destroy(context.Background()) + + pool, err = p1.server.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + panic(err) + } + + _ = pool + + return nil +} diff --git a/tests/plugins/rpc/config_test.go b/tests/plugins/rpc/config_test.go new file mode 100755 index 00000000..df5fa391 --- /dev/null +++ b/tests/plugins/rpc/config_test.go @@ -0,0 +1,135 @@ +package rpc + +import ( + "runtime" + "testing" + + "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/stretchr/testify/assert" +) + +func TestConfig_Listener(t *testing.T) { + cfg := &rpc.Config{Listen: "tcp://:18001"} + + ln, err := cfg.Listener() + assert.NoError(t, err) + assert.NotNil(t, ln) + defer func() { + err := ln.Close() + if err != nil { + t.Errorf("error closing the listener: error %v", err) + } + }() + + assert.Equal(t, "tcp", ln.Addr().Network()) + if runtime.GOOS == "windows" { + assert.Equal(t, "[::]:18001", ln.Addr().String()) + } else { + assert.Equal(t, "0.0.0.0:18001", ln.Addr().String()) + } +} + +func TestConfig_ListenerUnix(t *testing.T) { + cfg := &rpc.Config{Listen: "unix://file.sock"} + + ln, err := cfg.Listener() + assert.NoError(t, err) + assert.NotNil(t, ln) + defer func() { + err := ln.Close() + if err != nil { + t.Errorf("error closing the listener: error %v", err) + } + }() + + assert.Equal(t, "unix", ln.Addr().Network()) + assert.Equal(t, "file.sock", ln.Addr().String()) +} + +func Test_Config_Error(t *testing.T) { + cfg := &rpc.Config{Listen: "uni:unix.sock"} + ln, err := cfg.Listener() + assert.Nil(t, ln) + assert.Error(t, err) + assert.Equal(t, "invalid DSN (tcp://:6001, unix://file.sock)", err.Error()) +} + +func Test_Config_ErrorMethod(t *testing.T) { + cfg := &rpc.Config{Listen: "xinu://unix.sock"} + + ln, err := cfg.Listener() + assert.Nil(t, ln) + assert.Error(t, err) +} + +func TestConfig_Dialer(t *testing.T) { + cfg := &rpc.Config{Listen: "tcp://:18001"} + + ln, _ := cfg.Listener() + defer func() { + err := ln.Close() + if err != nil { + t.Errorf("error closing the listener: error %v", err) + } + }() + + conn, err := cfg.Dialer() + assert.NoError(t, err) + assert.NotNil(t, conn) + defer func() { + err := conn.Close() + if err != nil { + t.Errorf("error closing the connection: error %v", err) + } + }() + + assert.Equal(t, "tcp", conn.RemoteAddr().Network()) + assert.Equal(t, "127.0.0.1:18001", conn.RemoteAddr().String()) +} + +func TestConfig_DialerUnix(t *testing.T) { + cfg := &rpc.Config{Listen: "unix://file.sock"} + + ln, _ := cfg.Listener() + defer func() { + err := ln.Close() + if err != nil { + t.Errorf("error closing the listener: error %v", err) + } + }() + + conn, err := cfg.Dialer() + assert.NoError(t, err) + assert.NotNil(t, conn) + defer func() { + err := conn.Close() + if err != nil { + t.Errorf("error closing the connection: error %v", err) + } + }() + + assert.Equal(t, "unix", conn.RemoteAddr().Network()) + assert.Equal(t, "file.sock", conn.RemoteAddr().String()) +} + +func Test_Config_DialerError(t *testing.T) { + cfg := &rpc.Config{Listen: "uni:unix.sock"} + ln, err := cfg.Dialer() + assert.Nil(t, ln) + assert.Error(t, err) + assert.Equal(t, "invalid socket DSN (tcp://:6001, unix://file.sock)", err.Error()) +} + +func Test_Config_DialerErrorMethod(t *testing.T) { + cfg := &rpc.Config{Listen: "xinu://unix.sock"} + + ln, err := cfg.Dialer() + assert.Nil(t, ln) + assert.Error(t, err) +} + +func Test_Config_Defaults(t *testing.T) { + c := &rpc.Config{} + c.InitDefaults() + assert.Equal(t, "tcp://127.0.0.1:6001", c.Listen) +} diff --git a/tests/plugins/rpc/configs/.rr-rpc-disabled.yaml b/tests/plugins/rpc/configs/.rr-rpc-disabled.yaml new file mode 100644 index 00000000..d5c185e7 --- /dev/null +++ b/tests/plugins/rpc/configs/.rr-rpc-disabled.yaml @@ -0,0 +1,6 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: true +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/rpc/configs/.rr.yaml b/tests/plugins/rpc/configs/.rr.yaml new file mode 100644 index 00000000..d2cb6c70 --- /dev/null +++ b/tests/plugins/rpc/configs/.rr.yaml @@ -0,0 +1,6 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/rpc/plugin1.go b/tests/plugins/rpc/plugin1.go new file mode 100644 index 00000000..6843b396 --- /dev/null +++ b/tests/plugins/rpc/plugin1.go @@ -0,0 +1,42 @@ +package rpc + +import ( + "fmt" + + "github.com/spiral/roadrunner/v2/plugins/config" +) + +type Plugin1 struct { + config config.Configurer +} + +func (p1 *Plugin1) Init(cfg config.Configurer) error { + p1.config = cfg + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "rpc_test.plugin1" +} + +func (p1 *Plugin1) RPC() interface{} { + return &PluginRPC{srv: p1} +} + +type PluginRPC struct { + srv *Plugin1 +} + +func (r *PluginRPC) Hello(in string, out *string) error { + *out = fmt.Sprintf("Hello, username: %s", in) + return nil +} diff --git a/tests/plugins/rpc/plugin2.go b/tests/plugins/rpc/plugin2.go new file mode 100644 index 00000000..2c47158f --- /dev/null +++ b/tests/plugins/rpc/plugin2.go @@ -0,0 +1,53 @@ +package rpc + +import ( + "net" + "net/rpc" + "time" + + "github.com/spiral/errors" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" +) + +// plugin2 makes a call to the plugin1 via RPC +// this is just a simulation of external call FOR TEST +// you don't need to do such things :) +type Plugin2 struct { +} + +func (p2 *Plugin2) Init() error { + return nil +} + +func (p2 *Plugin2) Serve() chan error { + errCh := make(chan error, 1) + + go func() { + time.Sleep(time.Second * 3) + + conn, err := net.Dial("tcp", "127.0.0.1:6001") + if err != nil { + errCh <- errors.E(errors.Serve, err) + return + } + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret string + err = client.Call("rpc_test.plugin1.Hello", "Valery", &ret) + if err != nil { + errCh <- err + return + } + if ret != "Hello, username: Valery" { + errCh <- errors.E("wrong response") + return + } + // to stop exec + errCh <- errors.E(errors.Disabled) + }() + + return errCh +} + +func (p2 *Plugin2) Stop() error { + return nil +} diff --git a/tests/plugins/rpc/rpc_test.go b/tests/plugins/rpc/rpc_test.go new file mode 100644 index 00000000..98959b28 --- /dev/null +++ b/tests/plugins/rpc/rpc_test.go @@ -0,0 +1,188 @@ +package rpc + +import ( + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/stretchr/testify/assert" +) + +// graph https://bit.ly/3ensdNb +func TestRpcInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin1{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin2{}) + if err != nil { + t.Fatal(err) + } + + v := &config.Viper{} + v.Path = "configs/.rr.yaml" + v.Prefix = "rr" + err = cont.Register(v) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&rpc.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + if err != nil { + t.Fatal(err) + } + + sig := make(chan os.Signal, 1) + + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + defer tt.Stop() + for { + select { + case e := <-ch: + // just stop, this is ok + if errors.Is(errors.Disabled, e.Error) { + return + } + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + assert.Fail(t, "timeout") + } + } + }() + + wg.Wait() +} + +// graph https://bit.ly/3ensdNb +func TestRpcDisabled(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin1{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin2{}) + if err != nil { + t.Fatal(err) + } + + v := &config.Viper{} + v.Path = "configs/.rr-rpc-disabled.yaml" + v.Prefix = "rr" + err = cont.Register(v) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&rpc.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + if err != nil { + t.Fatal(err) + } + + sig := make(chan os.Signal, 1) + + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + tt := time.NewTimer(time.Second * 20) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + defer tt.Stop() + for { + select { + case e := <-ch: + // RPC is turned off, should be and dial error + if errors.Is(errors.Disabled, e.Error) { + assert.FailNow(t, "should not be disabled error") + } + assert.Error(t, e.Error) + err = cont.Stop() + assert.Error(t, err) + return + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + return + } + } + }() + + wg.Wait() +} diff --git a/tests/plugins/server/plugin_pipes.go b/tests/plugins/server/plugin_pipes.go index 6f31395a..5eb2fed1 100644 --- a/tests/plugins/server/plugin_pipes.go +++ b/tests/plugins/server/plugin_pipes.go @@ -5,12 +5,12 @@ import ( "time" "github.com/spiral/errors" - "github.com/spiral/roadrunner-plugins/config" "github.com/spiral/roadrunner/v2/interfaces/pool" "github.com/spiral/roadrunner/v2/pkg/payload" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" "github.com/spiral/roadrunner/v2/pkg/worker" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/server" ) const ConfigSection = "server" diff --git a/tests/plugins/server/plugin_sockets.go b/tests/plugins/server/plugin_sockets.go index 1820b4c1..ede67ded 100644 --- a/tests/plugins/server/plugin_sockets.go +++ b/tests/plugins/server/plugin_sockets.go @@ -4,11 +4,11 @@ import ( "context" "github.com/spiral/errors" - "github.com/spiral/roadrunner-plugins/config" "github.com/spiral/roadrunner/v2/interfaces/pool" "github.com/spiral/roadrunner/v2/pkg/payload" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" "github.com/spiral/roadrunner/v2/pkg/worker" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/server" ) type Foo2 struct { diff --git a/tests/plugins/server/plugin_tcp.go b/tests/plugins/server/plugin_tcp.go index 01fa0cf5..98c13b2b 100644 --- a/tests/plugins/server/plugin_tcp.go +++ b/tests/plugins/server/plugin_tcp.go @@ -4,11 +4,11 @@ import ( "context" "github.com/spiral/errors" - "github.com/spiral/roadrunner-plugins/config" "github.com/spiral/roadrunner/v2/interfaces/pool" "github.com/spiral/roadrunner/v2/pkg/payload" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" "github.com/spiral/roadrunner/v2/pkg/worker" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/server" ) type Foo3 struct { diff --git a/tests/plugins/server/server_test.go b/tests/plugins/server/server_plugin_test.go index 75111ef7..d63b0ccd 100644 --- a/tests/plugins/server/server_test.go +++ b/tests/plugins/server/server_plugin_test.go @@ -3,13 +3,14 @@ package server import ( "os" "os/signal" + "sync" "testing" "time" "github.com/spiral/endure" - "github.com/spiral/roadrunner-plugins/config" - "github.com/spiral/roadrunner-plugins/logger" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" "github.com/stretchr/testify/assert" ) @@ -56,27 +57,31 @@ func TestAppPipes(t *testing.T) { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) - // stop after 10 seconds - tt := time.NewTicker(time.Second * 10) - - for { - select { - case e := <-errCh: - assert.NoError(t, e.Error) - assert.NoError(t, container.Stop()) - return - case <-c: - er := container.Stop() - if er != nil { - panic(er) + tt := time.NewTimer(time.Second * 10) + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + defer tt.Stop() + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + assert.NoError(t, container.Stop()) + return + case <-c: + er := container.Stop() + assert.NoError(t, er) + return + case <-tt.C: + assert.NoError(t, container.Stop()) + return } - return - case <-tt.C: - tt.Stop() - assert.NoError(t, container.Stop()) - return } - } + }() + + wg.Wait() } func TestAppSockets(t *testing.T) { diff --git a/tests/plugins/static/config_test.go b/tests/plugins/static/config_test.go new file mode 100644 index 00000000..f458eed3 --- /dev/null +++ b/tests/plugins/static/config_test.go @@ -0,0 +1,49 @@ +package static + +import ( + "testing" + + "github.com/spiral/roadrunner/v2/plugins/static" + "github.com/stretchr/testify/assert" +) + +func TestConfig_Forbids(t *testing.T) { + cfg := static.Config{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "", Forbid: []string{".php"}, Always: nil, Request: nil, Response: nil}} + + assert.True(t, cfg.AlwaysForbid("index.php")) + assert.True(t, cfg.AlwaysForbid("index.PHP")) + assert.True(t, cfg.AlwaysForbid("phpadmin/index.bak.php")) + assert.False(t, cfg.AlwaysForbid("index.html")) +} + +func TestConfig_Valid(t *testing.T) { + assert.NoError(t, (&static.Config{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "./"}}).Valid()) + + assert.Error(t, (&static.Config{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "./config.go"}}).Valid()) + + assert.Error(t, (&static.Config{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "./dir/"}}).Valid()) +} diff --git a/tests/plugins/static/configs/.rr-http-static-disabled.yaml b/tests/plugins/static/configs/.rr-http-static-disabled.yaml new file mode 100644 index 00000000..d8ee15e0 --- /dev/null +++ b/tests/plugins/static/configs/.rr-http-static-disabled.yaml @@ -0,0 +1,33 @@ +server: + command: "php ../../http/client.php pid pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:21234 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "abc" #not exists + forbid: [ ".php", ".htaccess" ] + request: + "Example-Request-Header": "Value" + # Automatically add headers to every response. + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/static/configs/.rr-http-static-files-disable.yaml b/tests/plugins/static/configs/.rr-http-static-files-disable.yaml new file mode 100644 index 00000000..563d95cf --- /dev/null +++ b/tests/plugins/static/configs/.rr-http-static-files-disable.yaml @@ -0,0 +1,33 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:45877 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "../../../tests" + forbid: [ ".php" ] + request: + "Example-Request-Header": "Value" + # Automatically add headers to every response. + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/static/configs/.rr-http-static-files.yaml b/tests/plugins/static/configs/.rr-http-static-files.yaml new file mode 100644 index 00000000..8961c6f4 --- /dev/null +++ b/tests/plugins/static/configs/.rr-http-static-files.yaml @@ -0,0 +1,34 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:34653 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "../../../tests" + forbid: [ ".php", ".htaccess" ] + always: [ ".ico" ] + request: + "Example-Request-Header": "Value" + # Automatically add headers to every response. + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/static/configs/.rr-http-static.yaml b/tests/plugins/static/configs/.rr-http-static.yaml new file mode 100644 index 00000000..0a1f5df4 --- /dev/null +++ b/tests/plugins/static/configs/.rr-http-static.yaml @@ -0,0 +1,31 @@ +server: + command: "php ../../http/client.php pid pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + address: 127.0.0.1:21603 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "../../../tests" + forbid: [ "" ] + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/static/static_plugin_test.go b/tests/plugins/static/static_plugin_test.go new file mode 100644 index 00000000..74daaa16 --- /dev/null +++ b/tests/plugins/static/static_plugin_test.go @@ -0,0 +1,437 @@ +package static + +import ( + "bytes" + "io" + "io/ioutil" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/gzip" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/spiral/roadrunner/v2/plugins/static" + "github.com/spiral/roadrunner/v2/tests/mocks" + "github.com/stretchr/testify/assert" +) + +func TestStaticPlugin(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("ServeSample", serveStaticSample) + t.Run("StaticNotForbid", staticNotForbid) + t.Run("StaticHeaders", staticHeaders) + + stopCh <- struct{}{} + wg.Wait() +} + +func staticHeaders(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:21603/client.php", nil) + if err != nil { + t.Fatal(err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + + if resp.Header.Get("Output") != "output-header" { + t.Fatal("can't find output header in response") + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + + defer func() { + _ = resp.Body.Close() + }() + + assert.Equal(t, all("../../../tests/client.php"), string(b)) + assert.Equal(t, all("../../../tests/client.php"), string(b)) +} + +func staticNotForbid(t *testing.T) { + b, r, err := get("http://localhost:21603/client.php") + assert.NoError(t, err) + assert.Equal(t, all("../../../tests/client.php"), b) + assert.Equal(t, all("../../../tests/client.php"), b) + _ = r.Body.Close() +} + +func serveStaticSample(t *testing.T) { + b, r, err := get("http://localhost:21603/sample.txt") + assert.NoError(t, err) + assert.Equal(t, "sample", b) + _ = r.Body.Close() +} + +func TestStaticDisabled(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-disabled.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("StaticDisabled", staticDisabled) + + stopCh <- struct{}{} + wg.Wait() +} + +func staticDisabled(t *testing.T) { + _, r, err := get("http://localhost:21234/sample.txt") //nolint:bodyclose + assert.Error(t, err) + assert.Nil(t, r) +} + +func TestStaticFilesDisabled(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-files-disable.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("StaticFilesDisabled", staticFilesDisabled) + + stopCh <- struct{}{} + wg.Wait() +} + +func staticFilesDisabled(t *testing.T) { + b, r, err := get("http://localhost:45877/client.php?hello=world") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "WORLD", b) + _ = r.Body.Close() +} + +func TestStaticFilesForbid(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-files.yaml", + Prefix: "rr", + } + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").AnyTimes() + mockLogger.EXPECT().Error("file open error", "error", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("StaticTestFilesDir", staticTestFilesDir) + t.Run("StaticNotFound", staticNotFound) + t.Run("StaticFilesForbid", staticFilesForbid) + t.Run("StaticFilesAlways", staticFilesAlways) + + stopCh <- struct{}{} + wg.Wait() +} + +func staticTestFilesDir(t *testing.T) { + b, r, err := get("http://localhost:34653/http?hello=world") + assert.NoError(t, err) + assert.Equal(t, "WORLD", b) + _ = r.Body.Close() +} + +func staticNotFound(t *testing.T) { + b, _, _ := get("http://localhost:34653/client.XXX?hello=world") //nolint:bodyclose + assert.Equal(t, "WORLD", b) +} + +func staticFilesAlways(t *testing.T) { + _, r, err := get("http://localhost:34653/favicon.ico") + assert.NoError(t, err) + assert.Equal(t, 404, r.StatusCode) + _ = r.Body.Close() +} + +func staticFilesForbid(t *testing.T) { + b, r, err := get("http://localhost:34653/client.php?hello=world") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "WORLD", b) + _ = r.Body.Close() +} + +// HELPERS +func get(url string) (string, *http.Response, error) { + r, err := http.Get(url) //nolint:gosec + if err != nil { + return "", nil, err + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return "", nil, err + } + + err = r.Body.Close() + if err != nil { + return "", nil, err + } + + return string(b), r, err +} + +func all(fn string) string { + f, _ := os.Open(fn) + + b := new(bytes.Buffer) + _, err := io.Copy(b, f) + if err != nil { + return "" + } + + err = f.Close() + if err != nil { + return "" + } + + return b.String() +} |