Compare commits

..

244 Commits

Author SHA1 Message Date
bbbcb22565 fieldalignment of all structs to save memory
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-06 19:45:13 +03:00
cb70dfa664 meter/wrapper: use meter.DefaultMeter in NewOptions
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-05 17:40:03 +03:00
1f0482fbd5 tracer: finalize tracer implementation
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-04 01:12:16 +03:00
a862562284 fixup domain in ListServices
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-03 18:16:54 +03:00
c320c23913 metadata: minor fixup for NewXXXContext functions
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-01 13:00:53 +03:00
Renovate Bot
ae848ba8bb fix(deps): update golang.org/x/net commit hash to e18ecbb 2021-02-26 19:46:14 +00:00
Renovate Bot
8e264cbb3e fix(deps): update golang.org/x/net commit hash to 39120d0 2021-02-26 12:05:10 +00:00
Renovate Bot
54e523ab3f fix(deps): update golang.org/x/net commit hash to 3d97a24 2021-02-26 08:40:37 +00:00
09973af099 server: add error helper
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-22 00:52:18 +03:00
3247da3dd0 metadata: add Pairs helper func
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-22 00:08:05 +03:00
b505455f7c run go mod tidy in renovate update
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-21 23:55:19 +03:00
293949f081 metadata: add Append func to Incoming/Outgoing context
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-21 23:54:59 +03:00
8d7e442b3a server: add SubscriberBodyOnly option
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-20 18:12:13 +03:00
renovate[bot]
f7b5211af3 fix(deps): update golang.org/x/net commit hash to 5f55cee (#20)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-02-20 11:40:35 +03:00
7eb6d030dc meter: fix internal labels sorting
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-18 15:57:42 +03:00
47e75c31c7 meter: export labels len method
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-18 14:41:51 +03:00
20ff5eed22 meter: initial wrapper import (#19)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-18 14:35:10 +03:00
d23ca8db73 Merge pull request #18 from unistack-org/flow
flow: initial tests
2021-02-18 12:49:32 +03:00
4dd28ac720 go 1.15 => 1.16
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-18 12:49:02 +03:00
240b6016df flow: add initial flow dag
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-18 12:44:37 +03:00
cf2aa827e4 update to go 1.16
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-18 12:44:18 +03:00
5596345382 util/rand: replace all non crypto rand stuff with own rand package
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-18 12:44:18 +03:00
67748a2132 util/reflect: import own path based interface lookup
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-14 23:33:01 +03:00
c2333a9f35 gh actions not fail on lint errors
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-14 16:26:40 +03:00
4ec4c277b7 lint: fix all major issues
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-14 16:16:01 +03:00
a11dd00174 profiler: fix import
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-14 14:25:04 +03:00
cc7ebedf22 debug/profile: move to profiler interface
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-14 14:02:51 +03:00
e5bf1448f4 lint fixes
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-14 11:28:50 +03:00
f182bba6ff debug/log: remove stale files
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-13 20:01:57 +03:00
1f8810599b go.mod cleanup
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-13 15:37:33 +03:00
82248eb3b0 many lint fixes and optimizations (#17)
* util/kubernetes: drop stale files
* debug/log/kubernetes: drop stale files
* util/scope: remove stale files
* util/mdns: drop stale files
* lint fixes

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-13 15:35:56 +03:00
abb9937787 fix lint issues (#16)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-13 01:46:16 +03:00
fd5ed64729 metadata: fix nil metadata from FromIncoming/FromOutgoing context
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-12 17:10:35 +03:00
6751060d05 move memory implementations to core micro repo
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-12 16:33:16 +03:00
ef664607b4 automerge minor version updates
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-12 11:48:09 +03:00
62e482a14b move renovate
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-10 12:44:56 +03:00
a390ebf80f fix renovate.json
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-10 12:43:29 +03:00
9a44960be7 another fix
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-10 10:52:33 +03:00
c846c59b9b fix renovate.json
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-10 10:39:37 +03:00
renovate[bot]
902bf6326b chore(deps): update golangci/golangci-lint-action action to v2 (#14)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-02-10 00:35:56 +03:00
renovate[bot]
bddf3bf502 chore(deps): update actions/setup-go action to v2 (#13)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-02-09 23:04:23 +03:00
renovate[bot]
284131da98 Add renovate.json (#12)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-09 23:01:50 +03:00
927c7ea3c2 metadata: allow to modify metadata via SetXXX functions
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-09 12:46:14 +03:00
0e51a79bb6 metadata: split context to incoming and outgoing
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-09 01:08:45 +03:00
1de9911b73 util/reflect: add missing types for merge
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-06 18:13:43 +03:00
b4092c6619 util/reflect: improve merge for map
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-05 18:27:16 +03:00
024868bfd7 api: encode body param in endpoint
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-02 19:35:16 +03:00
a0bbfd6d02 provide compa options
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-02-02 15:37:12 +03:00
2cb6843773 codec: fix noop codec
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-29 23:18:12 +03:00
87e1480077 config: add name to each config imp
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-29 16:18:17 +03:00
bcd7f6a551 codec: fix noop codec to handle *broker.Message
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-29 16:07:21 +03:00
925b3af46b register: fix options
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-29 15:06:47 +03:00
ef4efa6a6b rename util
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-29 14:50:09 +03:00
125646d89b add Name func option
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-29 14:07:35 +03:00
7af7649448 store: add Name func
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-29 14:02:54 +03:00
827d467077 micro: rewrite options to support multiple building blocks
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-29 13:17:32 +03:00
ac8a3a12c4 meter: complete interface
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-27 00:54:19 +03:00
286785491c store: improve interface
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-26 02:09:26 +03:00
263ea8910d meter: use plan map and metadata
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-23 00:23:29 +03:00
202a942eef metadata: add Merge func
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-23 00:09:07 +03:00
c7bafecce3 add meter and tracer across all options
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-22 23:32:33 +03:00
c67fe6f330 meter: add option helper and provide default metric name and label prefix
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-22 19:18:28 +03:00
8c3f0d2c64 meter: remove wrapper
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-22 18:22:17 +03:00
8494178b0d meter: rework meter interface
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-22 18:21:40 +03:00
8a2c4c511e metadata: add iterator method
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-21 18:37:54 +03:00
dcca28944e util/reflect: add useful helpers
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-21 14:05:50 +03:00
92e6fd036e config: merge default not overwrite
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-21 02:20:06 +03:00
eab1a1dd40 api/server: move to dedicated repo
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-20 01:21:15 +03:00
188d9611c9 util/reflect: add struct field helper
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-20 00:47:13 +03:00
74a52eed9d rename metrics to meter
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-19 16:26:00 +03:00
770e8425bd config: move reflect stuff to util/reflect
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-19 01:40:34 +03:00
4783c6d9a3 client: add option helper
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-19 00:45:55 +03:00
2b2bcf4586 client: add call option helper
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-18 23:48:50 +03:00
77f517a9f6 metrics: add context option
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-15 22:47:28 +03:00
49d54f7fe6 logger: remove SetLevel from logger interface
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-12 09:26:14 +03:00
8b7380876e modify all code for never logger interface
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-10 19:24:03 +03:00
7b3a7a9448 logger: update logger interface
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-10 18:56:39 +03:00
270ad1b889 api: fix Decode method
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-10 03:55:04 +03:00
bcf7cf10d3 server: fix NewHandlerOptions
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-10 03:36:23 +03:00
8930c3fbb7 api/router: extend interface
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-01-10 01:54:21 +03:00
e6f870bda7 codec: fix interface
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-20 23:53:29 +03:00
8feab7cc48 config: init before load
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-20 23:08:40 +03:00
aa6afdf440 config: fix tests
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-20 01:07:48 +03:00
6b1ed63b48 guard agains not fully configured config sources
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-20 01:00:52 +03:00
b50855855b config: export method to init new empty struct
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-20 00:15:29 +03:00
150e8ad698 config: improve and export helper func
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-19 23:22:05 +03:00
035a84e696 config: fix tests
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-18 03:50:52 +03:00
565082f515 before/after config stuff handled by implementations
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-18 03:38:09 +03:00
8c504bd029 handle before/after inside config implementations
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-18 03:37:18 +03:00
f6c0728a59 fix context usage across codebase
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-17 22:52:00 +03:00
70a17dc10a client: allow to create new client with predefined call options
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-15 23:09:51 +03:00
f14efa64f0 server: add MaxConn and Listener options
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-15 11:52:05 +03:00
42f4d26fe4 server: add MaxConn option
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-15 11:46:30 +03:00
06c3cd6637 config: fix default config loader
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-13 19:28:29 +03:00
99738096ac fix configs options
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-13 18:56:54 +03:00
c6dfc8acaa load config on service init
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-13 14:36:31 +03:00
762f20d179 registry: adopt micro/micro 69b0ac2e9140fee1cde043f5ecdab438a41898ee
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-13 13:51:09 +03:00
92aec349c3 config: add missing codec error
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-13 13:26:44 +03:00
2dcd30b21c add defaut config parser
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-13 13:17:38 +03:00
a7a3c679d1 config: load defaults
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-13 13:10:04 +03:00
5c6eba20e7 codec: fix noop codec
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-11 00:32:29 +03:00
0a68a9c278 use default codec for store
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-11 00:21:53 +03:00
a13cb01005 store: create options helpers
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-10 22:37:40 +03:00
9fc0b5f88b store: remove unneded options
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-10 22:31:36 +03:00
6a7433ba2a store: refactor interface (#11)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-10 22:08:56 +03:00
a754ff7c0c more lint fixes
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-09 12:10:25 +03:00
e08276c2e2 gofmt
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-08 00:41:14 +03:00
b7b28f6b9a lint
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-08 00:38:37 +03:00
f63ff80d46 backport micro 820f59869617dee18c9a699c8e01d7a6b080f854
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-07 21:54:45 +03:00
8fd745eab0 config: add logger to options
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-07 19:27:08 +03:00
c7ed807129 default retries is always 0
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-07 19:26:22 +03:00
c6fd9c1c23 add context helpers
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-07 16:10:20 +03:00
b5d3b699cf fixup
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-04 02:34:20 +03:00
5279c2aa0f config package rework (#9)
* change config interface
* reuse codec
* allow to modify next config based on values in current config via BeforeLoad

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-12-04 02:28:45 +03:00
0ddc8de00b apply micro commit 2eb19c2e97d9316438bc66cd2cda896e8c99d026
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-30 22:48:00 +03:00
8d6eb34aee fix random selector
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-29 17:30:54 +03:00
0d93b2c31c add some defaults
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-26 01:13:05 +03:00
3f6852030f expose codec options
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-25 10:43:13 +03:00
458388359a add string support to noop codec
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-25 10:04:11 +03:00
2101e994d9 add Frame support in noop codec
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-25 09:59:28 +03:00
8a50a2d0b8 receives only body of the message
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-25 08:33:29 +03:00
71d82e9d5b add codec NewMessage helper
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-24 15:14:47 +03:00
c9049c3845 major codec upgrade
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-23 16:21:07 +03:00
daffa9e548 use metadata.Metadata (#8)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-18 16:50:41 +03:00
e0ef8b2953 merics: add Init func to interface
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-15 00:53:40 +03:00
f6c914c1e4 metrics: minor changes to interface and set default
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-15 00:38:38 +03:00
b38484d18e go mod tidy
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-13 15:06:12 +03:00
75222e07cb exttract protoc plugin to dedicated repo
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-13 14:59:14 +03:00
4233a4b673 init cmd and logger options on service init
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-13 14:52:19 +03:00
c44a82a8cb registry/noop: implement noop watcher
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-13 14:50:57 +03:00
37f7960f4a logger: add WithContext option
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-13 14:50:29 +03:00
6dc7e792c8 logger improvements
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-10 22:18:16 +03:00
81649d51e1 Merge branch 'master' of https://github.com/unistack-org/micro 2020-11-10 11:01:14 +03:00
23f5d10ccb server: remove unused code
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-10 11:00:37 +03:00
e3f235acc1 api/handler/rpc: fix corner cases
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-10 11:00:06 +03:00
fa9ef1c816 simplify service stop waiting
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-10 10:59:26 +03:00
77dab8ee15 server: remove unneded chan in noop server
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-10 10:58:32 +03:00
51fbff3e4a metadata: add checks for nil context
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-10 10:57:33 +03:00
bd6493327f Merge pull request #7 from itzmanish/protoc-gen-micro-fix
generate SendAndClose() and CloseAndRecv() on streams.
2020-11-07 15:53:00 +03:00
Manish
2141e9631c generate SendAndClose() and CloseAndRecv() on streams. 2020-11-07 14:55:33 +05:30
be8d09c663 network/transport: fix default dial timeout
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-06 11:18:12 +03:00
72bbbe3817 client: remove cache responses
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-06 00:25:09 +03:00
c92add984c allow to return all stuff from service opts
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-06 00:04:00 +03:00
3542d6c824 store: fix comment
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-06 00:03:40 +03:00
dc63d96e0b move store test to micro-tests repo
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-05 23:56:10 +03:00
7c9a7e84c7 not use selfsigned certs
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-05 23:55:53 +03:00
31180758b4 move avay grpc stuff to micro-server-grpc
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-05 23:46:11 +03:00
ce25a41fe1 remove cache as store can do the same thing
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-05 23:35:55 +03:00
8fa8afdfa4 fix namespace server issue
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-05 22:35:05 +03:00
e127547799 add registry helpers
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-05 21:18:05 +03:00
1fbf8b2e20 Merge pull request #6 from unistack-org/logger
rewrite logger
2020-11-05 00:49:03 +03:00
e41bb5ebc5 rewrite logger
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-05 00:23:05 +03:00
7c311aea19 api: extract routers to external repos
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-05 00:22:53 +03:00
8a2b122015 many fixes for lint and context.Context usage (#5)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-03 02:02:32 +03:00
40b0870cf8 fix linting (#4)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-03 01:08:23 +03:00
e6ab6d50eb remove unneeded Error field from broker.Message
close github.com/unistack-org/micro-roadmap/issues/26 (#3)

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-02 23:45:41 +03:00
a9eff06976 fix repocard issues (#2)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-11-02 13:25:29 +03:00
416fe5e4c8 run tests from micro-tests repo
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-30 23:52:05 +03:00
ddb53bf8e4 add slack badge
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-29 16:16:31 +03:00
0e6efda528 minor cleanup
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-28 17:48:39 +03:00
f2413a7789 disconnect from stuff on service stop
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-28 12:02:57 +03:00
9553f46cf4 connect to all stuff on start
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-28 10:51:09 +03:00
14c97d59c1 many improvements with options and noop stuff
* add many options helpers
* fix noop client to allow publish messages to topic in broker
* fix noop server to allow registering in registry
* fix noop server to allow subscribe to topic in broker
* fix new service initialization

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-16 09:38:57 +03:00
a59aae760f metadata: use new helper
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-12 17:20:52 +03:00
0a5b34a07b metadata: small optimization and export default metadata size
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-12 17:17:59 +03:00
62502ad720 always generate call with CamelCased name
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-12 16:19:30 +03:00
6e43ae7190 add client publish option func
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-12 12:09:09 +03:00
0e1f744fcc add helper for publish options
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-10 00:47:09 +03:00
2fc47782cf use metadata helper
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-09 16:51:58 +03:00
34d93306d6 new registry util func
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-09 16:49:29 +03:00
336868ed0d move helper to proper place
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-09 16:21:47 +03:00
2682f15b8e move helper to proper place
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-09 16:20:10 +03:00
4c12e38c01 move generic code from grpc server implementation
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-09 16:15:36 +03:00
62bfe9c06e allow to publish message via broker on noop client
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-09 13:43:04 +03:00
24be220f91 close micro-roadmap/issues/12
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-09 12:15:55 +03:00
cacd33e84f metadata: fix tests
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-01 16:05:05 +03:00
9475003059 fix concurrent map usage
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-10-01 16:00:01 +03:00
8532ccebba metadata: avoid allocations on delete
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-30 16:21:47 +03:00
9c55b1d06a fix metadata issues with uppercase letters
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-30 16:14:54 +03:00
efd9075d9b add error vars
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-28 13:08:53 +03:00
4c4fa00a5d add useful func
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-22 13:33:57 +03:00
21d5ca1cdd rename func
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-22 13:17:38 +03:00
ec3c1a02fc expose useful broker and server methods
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-22 13:15:05 +03:00
dc5dc6ab5b update
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-20 17:02:41 +03:00
1cbd1d2bad fix
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-20 16:57:54 +03:00
aa667728a1 move transport
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-20 16:52:26 +03:00
9b11ea527a add Wait option
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-20 15:48:07 +03:00
5787a1afb8 more useful router new options func
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-17 15:44:35 +03:00
74c10f1139 more idiomatic names inside generated protoc files
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-17 15:44:05 +03:00
7e3fac8937 fix store context issues
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-17 15:41:49 +03:00
6021edc855 add more context to store
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-17 15:18:01 +03:00
8817c110d0 add context to store
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-17 15:15:42 +03:00
d59db9df16 check for nil cmd
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-14 12:58:51 +03:00
2d1e6db9fd add context in server handler option
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-11 11:55:31 +03:00
5bfca99627 fix old proto
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-10 15:17:23 +03:00
9ea3149b60 fixup opts
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-10 00:25:33 +03:00
8f03480ed2 update go.mod
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-10 00:08:59 +03:00
caec730248 move out tracers
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-10 00:06:29 +03:00
f1fde75567 fix init
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-07 15:29:43 +03:00
5fe3a46732 init default stuff
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-07 13:38:52 +03:00
e7d418183b fixup logger usage (#33)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-05 02:43:16 +03:00
c576749b57 noop impl (#32)
* improve logger usage
* add noop client and server

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-05 02:11:29 +03:00
c062aab1a9 add noop broker and noop store (#30)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-09-03 15:11:05 +03:00
0252addf05 broker: resurrect DisableAutoAck
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-31 16:51:58 +03:00
b7338c12d7 broker: resurrect Queue option
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-31 16:48:29 +03:00
7c115823a7 Revert "fix protoc-gen-micro"
This reverts commit 47d007c0b6.
2020-08-31 11:51:30 +03:00
47d007c0b6 fix protoc-gen-micro
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-31 10:52:19 +03:00
c9b283be60 move out cache implementations
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-31 10:51:58 +03:00
112f21006c config: add default config
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-29 18:08:21 +03:00
53654185ba add logger to options
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-29 17:44:49 +03:00
2382446e10 registry: set noop as default registry
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-29 17:44:04 +03:00
24c20b6907 resolver/registry: small slice optimization
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-29 17:43:36 +03:00
622490fbf3 logger: refactor interface
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-29 17:43:06 +03:00
dd8894e673 registry: add noop registry
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-29 17:41:49 +03:00
eb1b14da8a model: split options, add logger
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-29 17:40:25 +03:00
0005f23585 errors: move grpc error def to grpc server
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-29 17:32:57 +03:00
fb233374a0 resolver/dns: support ipv6 addrs
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-29 16:53:53 +03:00
9c695ac343 split router implementations
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-28 12:19:47 +03:00
2c136b005e resurrect default for auth
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-28 11:55:18 +03:00
7cf42589b3 cleanup deps
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-28 11:52:51 +03:00
6aa857dfa1 fixup deps
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-28 11:27:45 +03:00
0f19355621 resurrect service stuff
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-28 10:58:08 +03:00
aa99378adc add check (#27)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-27 11:32:27 +03:00
b4ccde2228 resurrect broker event (#26)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-27 11:18:02 +03:00
36c53b4917 сleanup (#24)
* move out prometheus metrics
* not use gorilla wrappers

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-25 17:59:41 +03:00
ef773d8d49 remove util/log in favour of logger (#23)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-25 17:31:28 +03:00
fc3794f548 move runtime implementations (#22)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-25 15:55:30 +03:00
67ab44593b fix repocard issues (#21)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-25 15:41:48 +03:00
8076e410a9 fix repocard issues (#20)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-25 14:33:36 +03:00
dd78ae8658 cleanup (#18)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-25 14:16:08 +03:00
0f4b1435d9 move implementations to external repos (#17)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-25 13:44:41 +03:00
c4a303190a lint fixes (#14)
* lint fixes

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-21 14:53:21 +03:00
199ff66bd4 enable cache for build, closes #8 (#12)
* enable cache for build, closes #8
* goimports
* lint fixes

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-20 15:23:41 +03:00
98ba3b2788 grpc: avoid allocations for each message (#11)
* grpc: avoid allocations for each message
* fix tests for api/router

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-20 14:45:47 +03:00
5ea2590891 Merge pull request #10 from unistack-org/quic
update quic go package
2020-08-20 14:29:45 +03:00
0a42845a40 fix tests
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-20 14:20:27 +03:00
Asim Aslam
e6daa9a838 Update debug.go 2020-08-20 14:20:27 +03:00
ben-toogood
72621e0da4 events: update interface (#1954) 2020-08-20 14:20:27 +03:00
Dominic Wong
d5da9c0728 Unify the store tests (#1952)
Add more tests for store
2020-08-20 14:20:12 +03:00
Janos Dobronszki
eee5b98d78 Generic git checkout (#1951) 2020-08-20 14:18:53 +03:00
ben-toogood
cf084b410f util/file: allow context to be passed (#1950) 2020-08-20 14:18:52 +03:00
cf9bdd0f99 update quic go package
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-20 12:37:40 +03:00
06136312bb regen files with never protoc (#6)
* regen files with never protoc
* rewrite import path

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-19 17:47:17 +03:00
7a407d5792 setup lint (#3)
* setup lint

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2020-08-19 16:11:03 +03:00
560 changed files with 11472 additions and 54661 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
# These are supported funding model platforms
github: micro

2
.github/generate.sh vendored
View File

@@ -9,7 +9,7 @@ curl -s -o proto/google/api/http.proto -L https://raw.githubusercontent.com/goog
for PROTO in $PROTOS; do
echo $PROTO
protoc -I./proto -I. -I$(dirname $PROTO) --go_out=plugins=grpc,paths=source_relative:. --micro_out=paths=source_relative:. $PROTO
protoc -I./proto -I. -I$(dirname $PROTO) --go-grpc_out=paths=source_relative:. --go_out=paths=source_relative:. --micro_out=paths=source_relative:. $PROTO
done
rm -r proto

20
.github/renovate.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
"extends": [
"config:base"
],
"postUpdateOptions": ["gomodTidy"],
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
"automerge": true
},
{
"groupName": "all deps",
"separateMajorMinor": true,
"groupSlug": "all",
"packagePatterns": [
"*"
]
}
]
}

13
.github/stale.sh vendored Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash -ex
export PATH=$PATH:$(pwd)/bin
export GO111MODULE=on
export GOBIN=$(pwd)/bin
#go get github.com/rvflash/goup@v0.4.1
#goup -v ./...
#go get github.com/psampaz/go-mod-outdated@v0.6.0
go list -u -m -mod=mod -json all | go-mod-outdated -update -direct -ci || true
#go list -u -m -json all | go-mod-outdated -update

62
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: build
on:
push:
branches:
- master
jobs:
test:
name: test
runs-on: ubuntu-latest
steps:
- name: setup
uses: actions/setup-go@v2
with:
go-version: 1.16
- name: cache
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-
- name: sdk checkout
uses: actions/checkout@v2
- name: sdk deps
run: go get -v -t -d ./...
- name: sdk test
env:
INTEGRATION_TESTS: yes
run: go test -mod readonly -v ./...
- name: tests checkout
uses: actions/checkout@v2
with:
repository: unistack-org/micro-tests
ref: refs/heads/master
path: micro-tests
fetch-depth: 1
- name: tests deps
run: |
cd micro-tests
go mod edit -replace="github.com/unistack-org/micro/v3=../"
go get -v -t -d ./...
- name: tests test
env:
INTEGRATION_TESTS: yes
run: cd micro-tests && go test -mod readonly -v ./...
lint:
name: lint
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: lint
uses: golangci/golangci-lint-action@v2
continue-on-error: true
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.30
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: golangci-lint command line arguments.
# args: --issues-exit-code=0
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true

View File

@@ -1,22 +0,0 @@
name: Docker
on:
push:
branches:
- master
tags:
- v2.*
- v3.*
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
name: Check out repository
- uses: elgohr/Publish-Docker-Github-Action@2.12
name: Build and Push Docker Image
with:
name: micro/go-micro
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
tag_names: true

View File

@@ -1,31 +1,62 @@
name: PR Sanity Check
on: pull_request
name: prbuild
on:
pull_request:
branches:
- master
jobs:
prtest:
name: PR sanity check
test:
name: test
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
- name: setup
uses: actions/setup-go@v2
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
go-version: 1.16
- name: cache
uses: actions/cache@v2
with:
path: ~/go/pkg
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-
- name: sdk checkout
uses: actions/checkout@v2
- name: Get dependencies
run: |
go get -v -t -d ./...
- name: Run tests
id: tests
- name: sdk deps
run: go get -v -t -d ./...
- name: sdk test
env:
IN_TRAVIS_CI: yes
INTEGRATION_TESTS: yes
run: go test -mod readonly -v ./...
- name: tests checkout
uses: actions/checkout@v2
with:
repository: unistack-org/micro-tests
ref: refs/heads/master
path: micro-tests
fetch-depth: 1
- name: tests deps
run: |
wget -qO- https://binaries.cockroachdb.com/cockroach-v20.1.4.linux-amd64.tgz | tar xvz
cockroach-v20.1.4.linux-amd64/cockroach start-single-node --insecure &
go test -v ./...
cd micro-tests
go mod edit -replace="github.com/unistack-org/micro/v3=../"
go get -v -t -d ./...
- name: tests test
env:
INTEGRATION_TESTS: yes
run: cd micro-tests && go test -mod readonly -v ./...
lint:
name: lint
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: lint
uses: golangci/golangci-lint-action@v2
continue-on-error: true
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.30
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: golangci-lint command line arguments.
# args: --issues-exit-code=0
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true

View File

@@ -1,41 +0,0 @@
#!/bin/bash
# set -x
function build_binary {
echo building $1
pushd $1
go build -o _main
local ret=$?
if [ $ret -gt 0 ]; then
failed=1
failed_arr+=($1)
fi
popd
}
function is_main {
grep "package main" -l -dskip $1/*.go > /dev/null 2>&1
}
function check_dir {
is_main $1
local ret=$?
if [ $ret == 0 ]; then
build_binary $1 $2
fi
for filename in $1/*; do
if [ -d $filename ]; then
check_dir $filename $2
fi
done
}
failed_arr=()
failed=0
go mod edit -replace github.com/micro/go-micro/v2=github.com/$2/v2@$1
check_dir . $1
if [ $failed -gt 0 ]; then
echo Some builds failed
printf '%s\n' "${failed_arr[@]}"
fi
exit $failed

View File

@@ -1,19 +0,0 @@
#!/bin/bash
# set -x
failed=0
go mod edit -replace github.com/micro/go-micro/v2=github.com/$2/v2@$1
# basic test, build the binary
go install
failed=$?
if [ $failed -gt 0 ]; then
exit $failed
fi
# unit tests
IN_TRAVIS_CI=yes go test -v ./...
./scripts/test-docker.sh
# Generate keys for JWT tests
ssh-keygen -f /tmp/sshkey -m pkcs8 -q -N ""
ssh-keygen -f /tmp/sshkey -e -m pkcs8 > /tmp/sshkey.pub
go clean -testcache && IN_TRAVIS_CI=yes go test --tags=integration -v ./test

View File

@@ -1,65 +0,0 @@
name: Run tests
on: [push]
jobs:
test:
name: Test repo
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Setup Kind
uses: engineerd/setup-kind@v0.4.0
with:
version: v0.8.1
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: |
go get -v -t -d ./...
- name: Run tests
id: tests
env:
IN_TRAVIS_CI: yes
run: |
kubectl apply -f runtime/kubernetes/test/test.yaml
sudo mkdir -p /var/run/secrets/kubernetes.io/serviceaccount
sudo chmod 777 /var/run/secrets/kubernetes.io/serviceaccount
wget -qO- https://binaries.cockroachdb.com/cockroach-v20.1.4.linux-amd64.tgz | tar -xvz
cockroach-v20.1.4.linux-amd64/cockroach start-single-node --insecure &
wget -q https://github.com/nats-io/nats-streaming-server/releases/download/v0.18.0/nats-streaming-server-v0.18.0-linux-amd64.zip
unzip ./nats-streaming-server-v0.18.0-linux-amd64.zip
export PATH=$PATH:./nats-streaming-server-v0.18.0-linux-amd64
nats-streaming-server &
go test -tags kubernetes,nats -v ./...
- name: Notify of test failure
if: failure()
uses: rtCamp/action-slack-notify@v2.0.0
env:
SLACK_CHANNEL: build
SLACK_COLOR: '#BF280A'
SLACK_ICON: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
SLACK_TITLE: Tests Failed
SLACK_USERNAME: GitHub Actions
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
- name: Notify of test success
if: success()
uses: rtCamp/action-slack-notify@v2.0.0
env:
SLACK_CHANNEL: build
SLACK_COLOR: '#1FAD2B'
SLACK_ICON: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
SLACK_TITLE: Tests Passed
SLACK_USERNAME: GitHub Actions
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}

View File

@@ -1,5 +1,9 @@
run:
deadline: 10m
deadline: 5m
modules-download-mode: readonly
skip-files:
- ".*\\.pb\\.go$"
- ".*\\.pb\\.micro\\.go$"
linters:
disable-all: false
enable-all: false

1
CNAME
View File

@@ -1 +0,0 @@
go-micro.dev

View File

@@ -1,13 +0,0 @@
FROM golang:1.13-alpine
RUN mkdir /user && \
echo 'nobody:x:65534:65534:nobody:/:' > /user/passwd && \
echo 'nobody:x:65534:' > /user/group
ENV GO111MODULE=on
RUN apk --no-cache add make git gcc libtool musl-dev ca-certificates dumb-init && \
rm -rf /var/cache/apk/* /tmp/*
WORKDIR /
COPY ./go.mod ./go.sum ./
RUN go mod download && rm go.mod go.sum

View File

@@ -176,7 +176,8 @@
END OF TERMS AND CONDITIONS
Copyright 2015 Asim Aslam.
Copyright 2015-2020 Asim Aslam.
Copyright 2019-2020 Unistack LLC.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -1,16 +1,14 @@
# Go Micro [![License](https://img.shields.io/:license-apache-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Go.Dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/micro/go-micro/v3?tab=overview) [![Travis CI](https://api.travis-ci.org/micro/go-micro.svg?branch=master)](https://travis-ci.org/micro/go-micro) [![Go Report Card](https://goreportcard.com/badge/micro/go-micro)](https://goreportcard.com/report/github.com/micro/go-micro)
# Micro [![License](https://img.shields.io/:license-apache-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Doc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/unistack-org/micro/v3?tab=overview) [![Status](https://github.com/unistack-org/micro/workflows/build/badge.svg?branch=master)](https://github.com/unistack-org/micro/actions?query=workflow%3Abuild+branch%3Amaster+event%3Apush) [![Lint](https://goreportcard.com/badge/github.com/unistack-org/micro)](https://goreportcard.com/report/github.com/unistack-org/micro) [![Slack](https://img.shields.io/static/v1?label=micro&message=slack&color=blueviolet)](https://unistack-org.slack.com/messages/default)
Go Micro is a standard library for microservices.
Micro is a standard library for microservices.
## Overview
Go Micro provides the core requirements for distributed systems development including RPC and Event driven communication.
The **Micro** philosophy is sane defaults with a pluggable architecture. We provide defaults to get you started quickly
but everything can be easily swapped out.
Micro provides the core requirements for distributed systems development including RPC and Event driven communication.
## Features
Go Micro abstracts away the details of distributed systems. Here are the main features.
Micro abstracts away the details of distributed systems. Here are the main features.
- **Authentication** - Auth is built in as a first class citizen. Authentication and authorization enable secure
zero trust networking by providing every service an identity and certificates. This additionally includes rule
@@ -23,8 +21,7 @@ level config from any source such as env vars, file, etcd. You can merge the sou
CockroachDB by default. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework.
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
development. When service A needs to speak to service B it needs the location of that service. The default discovery mechanism is
multicast DNS (mdns), a zeroconf system.
development. When service A needs to speak to service B it needs the location of that service.
- **Load Balancing** - Client side load balancing built on service discovery. Once we have the addresses of any number of instances
of a service we now need a way to decide which node to route to. We use random hashed load balancing to provide even distribution
@@ -32,25 +29,24 @@ across the services and retry a different node if there's a problem.
- **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client
and server handle this by default. This includes protobuf and json by default.
and server handle this by default.
- **gRPC Transport** - gRPC based request/response with support for bidirectional streaming. We provide an abstraction for synchronous communication. A request made to a service will be automatically resolved, load balanced, dialled and streamed.
- **Transport** - gRPC or http based request/response with support for bidirectional streaming. We provide an abstraction for synchronous communication. A request made to a service will be automatically resolved, load balanced, dialled and streamed.
- **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.
Event notifications are a core pattern in micro service development. The default messaging system is a HTTP event message broker.
- **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.
Event notifications are a core pattern in micro service development.
- **Synchronization** - Distributed systems are often built in an eventually consistent manner. Support for distributed locking and
leadership are built in as a Sync interface. When using an eventually consistent database or scheduling use the Sync interface.
- **Pluggable Interfaces** - Go Micro makes use of Go interfaces for each distributed system abstraction. Because of this these interfaces
are pluggable and allows Go Micro to be runtime agnostic. You can plugin any underlying technology. Find plugins in
[github.com/micro/go-plugins](https://github.com/micro/go-plugins).
- **Pluggable Interfaces** - Micro makes use of Go interfaces for each system abstraction. Because of this these interfaces
are pluggable and allows Micro to be runtime agnostic.
## Getting Started
See [pkg.go.dev](https://pkg.go.dev/github.com/micro/go-micro/v3?tab=overview) for usage.
To be created.
## License
Go Micro is Apache 2.0 licensed.
Micro is Apache 2.0 licensed.

View File

@@ -1 +0,0 @@
theme: jekyll-theme-architect

View File

@@ -5,10 +5,12 @@ import (
"regexp"
"strings"
"github.com/micro/go-micro/v3/registry"
"github.com/micro/go-micro/v3/server"
"github.com/unistack-org/micro/v3/metadata"
"github.com/unistack-org/micro/v3/register"
"github.com/unistack-org/micro/v3/server"
)
// Api interface
type Api interface {
// Initialise options
Init(...Option) error
@@ -22,28 +24,30 @@ type Api interface {
String() string
}
// Options holds the options
type Options struct{}
// Option func signature
type Option func(*Options) error
// Endpoint is a mapping between an RPC method and HTTP endpoint
type Endpoint struct {
// RPC Method e.g. Greeter.Hello
// Name Greeter.Hello
Name string
// Description e.g what's this endpoint for
// Desciption for endpoint
Description string
// API Handler e.g rpc, proxy
// Handler e.g rpc, proxy
Handler string
// HTTP Host e.g example.com
Host []string
// HTTP Methods e.g GET, POST
Method []string
// HTTP Path e.g /greeter. Expect POSIX regex
Path []string
// Body destination
// "*" or "" - top level message value
// "string" - inner message value
Body string
// Host e.g example.com
Host []string
// Method e.g GET, POST
Method []string
// Path e.g /greeter. Expect POSIX regex
Path []string
// Stream flag
Stream bool
}
@@ -52,26 +56,10 @@ type Endpoint struct {
type Service struct {
// Name of service
Name string
// The endpoint for this service
// Endpoint for this service
Endpoint *Endpoint
// Versions of this service
Services []*registry.Service
}
func strip(s string) string {
return strings.TrimSpace(s)
}
func slice(s string) []string {
var sl []string
for _, p := range strings.Split(s, ",") {
if str := strip(p); len(str) > 0 {
sl = append(sl, strip(p))
}
}
return sl
// Services that provides service
Services []*register.Service
}
// Encode encodes an endpoint to endpoint metadata
@@ -97,24 +85,30 @@ func Encode(e *Endpoint) map[string]string {
set("method", strings.Join(e.Method, ","))
set("path", strings.Join(e.Path, ","))
set("host", strings.Join(e.Host, ","))
set("body", e.Body)
return ep
}
// Decode decodes endpoint metadata into an endpoint
func Decode(e map[string]string) *Endpoint {
func Decode(e metadata.Metadata) *Endpoint {
if e == nil {
return nil
}
return &Endpoint{
Name: e["endpoint"],
Description: e["description"],
Method: slice(e["method"]),
Path: slice(e["path"]),
Host: slice(e["host"]),
Handler: e["handler"],
}
ep := &Endpoint{}
ep.Name, _ = e.Get("endpoint")
ep.Description, _ = e.Get("description")
epmethod, _ := e.Get("method")
ep.Method = []string{epmethod}
eppath, _ := e.Get("path")
ep.Path = []string{eppath}
ephost, _ := e.Get("host")
ep.Host = []string{ephost}
ep.Handler, _ = e.Get("handler")
ep.Body, _ = e.Get("body")
return ep
}
// Validate validates an endpoint to guarantee it won't blow up when being served

View File

@@ -5,6 +5,7 @@ import (
"testing"
)
//nolint:gocyclo
func TestEncoding(t *testing.T) {
testData := []*Endpoint{
nil,

View File

@@ -1,121 +0,0 @@
// Package api provides an http-rpc handler which provides the entire http request over rpc
package api
import (
"net/http"
goapi "github.com/micro/go-micro/v3/api"
"github.com/micro/go-micro/v3/api/handler"
api "github.com/micro/go-micro/v3/api/proto"
"github.com/micro/go-micro/v3/client"
"github.com/micro/go-micro/v3/errors"
"github.com/micro/go-micro/v3/util/ctx"
"github.com/micro/go-micro/v3/util/router"
)
type apiHandler struct {
opts handler.Options
s *goapi.Service
}
const (
Handler = "api"
)
// API handler is the default handler which takes api.Request and returns api.Response
func (a *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
bsize := handler.DefaultMaxRecvSize
if a.opts.MaxRecvSize > 0 {
bsize = a.opts.MaxRecvSize
}
r.Body = http.MaxBytesReader(w, r.Body, bsize)
request, err := requestToProto(r)
if err != nil {
er := errors.InternalServerError("go.micro.api", err.Error())
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(500)
w.Write([]byte(er.Error()))
return
}
var service *goapi.Service
if a.s != nil {
// we were given the service
service = a.s
} else if a.opts.Router != nil {
// try get service from router
s, err := a.opts.Router.Route(r)
if err != nil {
er := errors.InternalServerError("go.micro.api", err.Error())
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(500)
w.Write([]byte(er.Error()))
return
}
service = s
} else {
// we have no way of routing the request
er := errors.InternalServerError("go.micro.api", "no route found")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(500)
w.Write([]byte(er.Error()))
return
}
// create request and response
c := a.opts.Client
req := c.NewRequest(service.Name, service.Endpoint.Name, request)
rsp := &api.Response{}
// create the context from headers
cx := ctx.FromRequest(r)
if err := c.Call(cx, req, rsp, client.WithRouter(router.New(service.Services))); err != nil {
w.Header().Set("Content-Type", "application/json")
ce := errors.Parse(err.Error())
switch ce.Code {
case 0:
w.WriteHeader(500)
default:
w.WriteHeader(int(ce.Code))
}
w.Write([]byte(ce.Error()))
return
} else if rsp.StatusCode == 0 {
rsp.StatusCode = http.StatusOK
}
for _, header := range rsp.GetHeader() {
for _, val := range header.Values {
w.Header().Add(header.Key, val)
}
}
if len(w.Header().Get("Content-Type")) == 0 {
w.Header().Set("Content-Type", "application/json")
}
w.WriteHeader(int(rsp.StatusCode))
w.Write([]byte(rsp.Body))
}
func (a *apiHandler) String() string {
return "api"
}
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &apiHandler{
opts: options,
}
}
func WithService(s *goapi.Service, opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &apiHandler{
opts: options,
s: s,
}
}

View File

@@ -1,109 +0,0 @@
package api
import (
"fmt"
"mime"
"net"
"net/http"
"strings"
api "github.com/micro/go-micro/v3/api/proto"
"github.com/oxtoacart/bpool"
)
var (
// need to calculate later to specify useful defaults
bufferPool = bpool.NewSizedBufferPool(1024, 8)
)
func requestToProto(r *http.Request) (*api.Request, error) {
if err := r.ParseForm(); err != nil {
return nil, fmt.Errorf("Error parsing form: %v", err)
}
req := &api.Request{
Path: r.URL.Path,
Method: r.Method,
Header: make(map[string]*api.Pair),
Get: make(map[string]*api.Pair),
Post: make(map[string]*api.Pair),
Url: r.URL.String(),
}
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
ct = "text/plain; charset=UTF-8" //default CT is text/plain
r.Header.Set("Content-Type", ct)
}
//set the body:
if r.Body != nil {
switch ct {
case "application/x-www-form-urlencoded":
// expect form vals in Post data
default:
buf := bufferPool.Get()
defer bufferPool.Put(buf)
if _, err = buf.ReadFrom(r.Body); err != nil {
return nil, err
}
req.Body = buf.String()
}
}
// Set X-Forwarded-For if it does not exist
if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
if prior, ok := r.Header["X-Forwarded-For"]; ok {
ip = strings.Join(prior, ", ") + ", " + ip
}
// Set the header
req.Header["X-Forwarded-For"] = &api.Pair{
Key: "X-Forwarded-For",
Values: []string{ip},
}
}
// Host is stripped from net/http Headers so let's add it
req.Header["Host"] = &api.Pair{
Key: "Host",
Values: []string{r.Host},
}
// Get data
for key, vals := range r.URL.Query() {
header, ok := req.Get[key]
if !ok {
header = &api.Pair{
Key: key,
}
req.Get[key] = header
}
header.Values = vals
}
// Post data
for key, vals := range r.PostForm {
header, ok := req.Post[key]
if !ok {
header = &api.Pair{
Key: key,
}
req.Post[key] = header
}
header.Values = vals
}
for key, vals := range r.Header {
header, ok := req.Header[key]
if !ok {
header = &api.Pair{
Key: key,
}
req.Header[key] = header
}
header.Values = vals
}
return req, nil
}

View File

@@ -1,46 +0,0 @@
package api
import (
"net/http"
"net/url"
"testing"
)
func TestRequestToProto(t *testing.T) {
testData := []*http.Request{
{
Method: "GET",
Header: http.Header{
"Header": []string{"test"},
},
URL: &url.URL{
Scheme: "http",
Host: "localhost",
Path: "/foo/bar",
RawQuery: "param1=value1",
},
},
}
for _, d := range testData {
p, err := requestToProto(d)
if err != nil {
t.Fatal(err)
}
if p.Path != d.URL.Path {
t.Fatalf("Expected path %s got %s", d.URL.Path, p.Path)
}
if p.Method != d.Method {
t.Fatalf("Expected method %s got %s", d.Method, p.Method)
}
for k, v := range d.Header {
if val, ok := p.Header[k]; !ok {
t.Fatalf("Expected header %s", k)
} else {
if val.Values[0] != v[0] {
t.Fatalf("Expected val %s, got %s", val.Values[0], v[0])
}
}
}
}
}

View File

@@ -1,141 +0,0 @@
// Package event provides a handler which publishes an event
package event
import (
"encoding/json"
"fmt"
"net/http"
"path"
"regexp"
"strings"
"time"
"github.com/google/uuid"
"github.com/micro/go-micro/v3/api/handler"
proto "github.com/micro/go-micro/v3/api/proto"
"github.com/micro/go-micro/v3/util/ctx"
"github.com/oxtoacart/bpool"
)
var (
bufferPool = bpool.NewSizedBufferPool(1024, 8)
)
type event struct {
opts handler.Options
}
var (
Handler = "event"
versionRe = regexp.MustCompilePOSIX("^v[0-9]+$")
)
func eventName(parts []string) string {
return strings.Join(parts, ".")
}
func evRoute(ns, p string) (string, string) {
p = path.Clean(p)
p = strings.TrimPrefix(p, "/")
if len(p) == 0 {
return ns, "event"
}
parts := strings.Split(p, "/")
// no path
if len(parts) == 0 {
// topic: namespace
// action: event
return strings.Trim(ns, "."), "event"
}
// Treat /v[0-9]+ as versioning
// /v1/foo/bar => topic: v1.foo action: bar
if len(parts) >= 2 && versionRe.Match([]byte(parts[0])) {
topic := ns + "." + strings.Join(parts[:2], ".")
action := eventName(parts[1:])
return topic, action
}
// /foo => topic: ns.foo action: foo
// /foo/bar => topic: ns.foo action: bar
topic := ns + "." + strings.Join(parts[:1], ".")
action := eventName(parts[1:])
return topic, action
}
func (e *event) ServeHTTP(w http.ResponseWriter, r *http.Request) {
bsize := handler.DefaultMaxRecvSize
if e.opts.MaxRecvSize > 0 {
bsize = e.opts.MaxRecvSize
}
r.Body = http.MaxBytesReader(w, r.Body, bsize)
// request to topic:event
// create event
// publish to topic
topic, action := evRoute(e.opts.Namespace, r.URL.Path)
// create event
ev := &proto.Event{
Name: action,
// TODO: dedupe event
Id: fmt.Sprintf("%s-%s-%s", topic, action, uuid.New().String()),
Header: make(map[string]*proto.Pair),
Timestamp: time.Now().Unix(),
}
// set headers
for key, vals := range r.Header {
header, ok := ev.Header[key]
if !ok {
header = &proto.Pair{
Key: key,
}
ev.Header[key] = header
}
header.Values = vals
}
// set body
if r.Method == "GET" {
bytes, _ := json.Marshal(r.URL.Query())
ev.Data = string(bytes)
} else {
// Read body
buf := bufferPool.Get()
defer bufferPool.Put(buf)
if _, err := buf.ReadFrom(r.Body); err != nil {
http.Error(w, err.Error(), 500)
return
}
ev.Data = buf.String()
}
// get client
c := e.opts.Client
// create publication
p := c.NewMessage(topic, ev)
// publish event
if err := c.Publish(ctx.FromRequest(r), p); err != nil {
http.Error(w, err.Error(), 500)
return
}
}
func (e *event) String() string {
return "event"
}
func NewHandler(opts ...handler.Option) handler.Handler {
return &event{
opts: handler.NewOptions(opts...),
}
}

View File

@@ -1,104 +0,0 @@
// Package http is a http reverse proxy handler
package http
import (
"errors"
"fmt"
"math/rand"
"net/http"
"net/http/httputil"
"net/url"
"github.com/micro/go-micro/v3/api"
"github.com/micro/go-micro/v3/api/handler"
"github.com/micro/go-micro/v3/registry"
)
const (
Handler = "http"
)
type httpHandler struct {
options handler.Options
// set with different initializer
s *api.Service
}
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
service, err := h.getService(r)
if err != nil {
w.WriteHeader(500)
return
}
if len(service) == 0 {
w.WriteHeader(404)
return
}
rp, err := url.Parse(service)
if err != nil {
w.WriteHeader(500)
return
}
httputil.NewSingleHostReverseProxy(rp).ServeHTTP(w, r)
}
// getService returns the service for this request from the selector
func (h *httpHandler) getService(r *http.Request) (string, error) {
var service *api.Service
if h.s != nil {
// we were given the service
service = h.s
} else if h.options.Router != nil {
// try get service from router
s, err := h.options.Router.Route(r)
if err != nil {
return "", err
}
service = s
} else {
// we have no way of routing the request
return "", errors.New("no route found")
}
// get the nodes for this service
var nodes []*registry.Node
for _, srv := range service.Services {
nodes = append(nodes, srv.Nodes...)
}
// select a random node
if len(nodes) == 0 {
return "", errors.New("no route found")
}
node := nodes[rand.Int()%len(nodes)]
return fmt.Sprintf("http://%s", node.Address), nil
}
func (h *httpHandler) String() string {
return "http"
}
// NewHandler returns a http proxy handler
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &httpHandler{
options: options,
}
}
// WithService creates a handler with a service
func WithService(s *api.Service, opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &httpHandler{
options: options,
s: s,
}
}

View File

@@ -1,127 +0,0 @@
package http
import (
"net"
"net/http"
"net/http/httptest"
"testing"
"github.com/micro/go-micro/v3/api/handler"
"github.com/micro/go-micro/v3/api/resolver"
"github.com/micro/go-micro/v3/api/resolver/vpath"
"github.com/micro/go-micro/v3/api/router"
regRouter "github.com/micro/go-micro/v3/api/router/registry"
"github.com/micro/go-micro/v3/registry"
"github.com/micro/go-micro/v3/registry/memory"
)
func testHttp(t *testing.T, path, service, ns string) {
r := memory.NewRegistry()
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
s := &registry.Service{
Name: service,
Nodes: []*registry.Node{
{
Id: service + "-1",
Address: l.Addr().String(),
},
},
}
r.Register(s)
defer r.Deregister(s)
// setup the test handler
m := http.NewServeMux()
m.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`you got served`))
})
// start http test serve
go http.Serve(l, m)
// create new request and writer
w := httptest.NewRecorder()
req, err := http.NewRequest("POST", path, nil)
if err != nil {
t.Fatal(err)
}
// initialise the handler
rt := regRouter.NewRouter(
router.WithHandler("http"),
router.WithRegistry(r),
router.WithResolver(vpath.NewResolver(
resolver.WithServicePrefix(ns),
)),
)
p := NewHandler(handler.WithRouter(rt))
// execute the handler
p.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("Expected 200 response got %d %s", w.Code, w.Body.String())
}
if w.Body.String() != "you got served" {
t.Fatalf("Expected body: you got served. Got: %s", w.Body.String())
}
}
func TestHttpHandler(t *testing.T) {
testData := []struct {
path string
service string
namespace string
}{
{
"/test/foo",
"go.micro.api.test",
"go.micro.api",
},
{
"/test/foo/baz",
"go.micro.api.test",
"go.micro.api",
},
{
"/v1/foo",
"go.micro.api.v1.foo",
"go.micro.api",
},
{
"/v1/foo/bar",
"go.micro.api.v1.foo",
"go.micro.api",
},
{
"/v2/baz",
"go.micro.api.v2.baz",
"go.micro.api",
},
{
"/v2/baz/bar",
"go.micro.api.v2.baz",
"go.micro.api",
},
{
"/v2/baz/bar",
"v2.baz",
"",
},
}
for _, d := range testData {
t.Run(d.service, func(t *testing.T) {
testHttp(t, d.path, d.service, d.namespace)
})
}
}

View File

@@ -1,44 +1,45 @@
package handler
import (
"github.com/micro/go-micro/v3/api/router"
"github.com/micro/go-micro/v3/client"
"github.com/micro/go-micro/v3/client/grpc"
"github.com/unistack-org/micro/v3/api/router"
"github.com/unistack-org/micro/v3/client"
"github.com/unistack-org/micro/v3/logger"
)
var (
// DefaultMaxRecvSize specifies max recv size for handler
DefaultMaxRecvSize int64 = 1024 * 1024 * 100 // 10Mb
)
// Options struct holds handler options
type Options struct {
MaxRecvSize int64
Namespace string
Router router.Router
Client client.Client
Logger logger.Logger
Namespace string
MaxRecvSize int64
}
// Option func signature
type Option func(o *Options)
// NewOptions fills in the blanks
// NewOptions creates new options struct and fills it
func NewOptions(opts ...Option) Options {
var options Options
options := Options{
Client: client.DefaultClient,
Router: router.DefaultRouter,
Logger: logger.DefaultLogger,
MaxRecvSize: DefaultMaxRecvSize,
}
for _, o := range opts {
o(&options)
}
if options.Client == nil {
WithClient(grpc.NewClient())(&options)
}
// set namespace if blank
if len(options.Namespace) == 0 {
WithNamespace("go.micro.api")(&options)
}
if options.MaxRecvSize == 0 {
options.MaxRecvSize = DefaultMaxRecvSize
}
return options
}
@@ -56,6 +57,7 @@ func WithRouter(r router.Router) Option {
}
}
// WithClient specifies client to be used by the handler
func WithClient(c client.Client) Option {
return func(o *Options) {
o.Client = c

View File

@@ -1,493 +0,0 @@
// Package rpc is a go-micro rpc handler.
package rpc
import (
"encoding/json"
"io"
"net/http"
"strconv"
"strings"
jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/micro/go-micro/v3/api"
"github.com/micro/go-micro/v3/api/handler"
"github.com/micro/go-micro/v3/api/internal/proto"
"github.com/micro/go-micro/v3/client"
"github.com/micro/go-micro/v3/codec"
"github.com/micro/go-micro/v3/codec/jsonrpc"
"github.com/micro/go-micro/v3/codec/protorpc"
"github.com/micro/go-micro/v3/errors"
"github.com/micro/go-micro/v3/logger"
"github.com/micro/go-micro/v3/metadata"
"github.com/micro/go-micro/v3/util/ctx"
"github.com/micro/go-micro/v3/util/qson"
"github.com/micro/go-micro/v3/util/router"
"github.com/oxtoacart/bpool"
)
const (
Handler = "rpc"
)
var (
// supported json codecs
jsonCodecs = []string{
"application/grpc+json",
"application/json",
"application/json-rpc",
}
// support proto codecs
protoCodecs = []string{
"application/grpc",
"application/grpc+proto",
"application/proto",
"application/protobuf",
"application/proto-rpc",
"application/octet-stream",
}
bufferPool = bpool.NewSizedBufferPool(1024, 8)
)
type rpcHandler struct {
opts handler.Options
s *api.Service
}
type buffer struct {
io.ReadCloser
}
func (b *buffer) Write(_ []byte) (int, error) {
return 0, nil
}
func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
bsize := handler.DefaultMaxRecvSize
if h.opts.MaxRecvSize > 0 {
bsize = h.opts.MaxRecvSize
}
r.Body = http.MaxBytesReader(w, r.Body, bsize)
defer r.Body.Close()
var service *api.Service
if h.s != nil {
// we were given the service
service = h.s
} else if h.opts.Router != nil {
// try get service from router
s, err := h.opts.Router.Route(r)
if err != nil {
writeError(w, r, errors.InternalServerError("go.micro.api", err.Error()))
return
}
service = s
} else {
// we have no way of routing the request
writeError(w, r, errors.InternalServerError("go.micro.api", "no route found"))
return
}
ct := r.Header.Get("Content-Type")
// Strip charset from Content-Type (like `application/json; charset=UTF-8`)
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
ct = ct[:idx]
}
// micro client
c := h.opts.Client
// create context
cx := ctx.FromRequest(r)
// set merged context to request
*r = *r.Clone(cx)
// if stream we currently only support json
if isStream(r, service) {
serveWebsocket(cx, w, r, service, c)
return
}
// create custom router
callOpt := client.WithRouter(router.New(service.Services))
// walk the standard call path
// get payload
br, err := requestPayload(r)
if err != nil {
writeError(w, r, err)
return
}
var rsp []byte
switch {
// proto codecs
case hasCodec(ct, protoCodecs):
request := &proto.Message{}
// if the extracted payload isn't empty lets use it
if len(br) > 0 {
request = proto.NewMessage(br)
}
// create request/response
response := &proto.Message{}
req := c.NewRequest(
service.Name,
service.Endpoint.Name,
request,
client.WithContentType(ct),
)
// make the call
if err := c.Call(cx, req, response, callOpt); err != nil {
writeError(w, r, err)
return
}
// marshall response
rsp, err = response.Marshal()
if err != nil {
writeError(w, r, err)
return
}
default:
// if json codec is not present set to json
if !hasCodec(ct, jsonCodecs) {
ct = "application/json"
}
// default to trying json
var request json.RawMessage
// if the extracted payload isn't empty lets use it
if len(br) > 0 {
request = json.RawMessage(br)
}
// create request/response
var response json.RawMessage
req := c.NewRequest(
service.Name,
service.Endpoint.Name,
&request,
client.WithContentType(ct),
)
// make the call
if err := c.Call(cx, req, &response, callOpt); err != nil {
writeError(w, r, err)
return
}
// marshall response
rsp, err = response.MarshalJSON()
if err != nil {
writeError(w, r, err)
return
}
}
// write the response
writeResponse(w, r, rsp)
}
func (rh *rpcHandler) String() string {
return "rpc"
}
func hasCodec(ct string, codecs []string) bool {
for _, codec := range codecs {
if ct == codec {
return true
}
}
return false
}
// requestPayload takes a *http.Request.
// If the request is a GET the query string parameters are extracted and marshaled to JSON and the raw bytes are returned.
// If the request method is a POST the request body is read and returned
func requestPayload(r *http.Request) ([]byte, error) {
var err error
// we have to decode json-rpc and proto-rpc because we suck
// well actually because there's no proxy codec right now
ct := r.Header.Get("Content-Type")
switch {
case strings.Contains(ct, "application/json-rpc"):
msg := codec.Message{
Type: codec.Request,
Header: make(map[string]string),
}
c := jsonrpc.NewCodec(&buffer{r.Body})
if err = c.ReadHeader(&msg, codec.Request); err != nil {
return nil, err
}
var raw json.RawMessage
if err = c.ReadBody(&raw); err != nil {
return nil, err
}
return ([]byte)(raw), nil
case strings.Contains(ct, "application/proto-rpc"), strings.Contains(ct, "application/octet-stream"):
msg := codec.Message{
Type: codec.Request,
Header: make(map[string]string),
}
c := protorpc.NewCodec(&buffer{r.Body})
if err = c.ReadHeader(&msg, codec.Request); err != nil {
return nil, err
}
var raw proto.Message
if err = c.ReadBody(&raw); err != nil {
return nil, err
}
return raw.Marshal()
case strings.Contains(ct, "application/x-www-form-urlencoded"):
r.ParseForm()
// generate a new set of values from the form
vals := make(map[string]string)
for k, v := range r.Form {
vals[k] = strings.Join(v, ",")
}
// marshal
return json.Marshal(vals)
// TODO: application/grpc
}
// otherwise as per usual
ctx := r.Context()
// dont user metadata.FromContext as it mangles names
md, ok := metadata.FromContext(ctx)
if !ok {
md = make(map[string]string)
}
// allocate maximum
matches := make(map[string]interface{}, len(md))
bodydst := ""
// get fields from url path
for k, v := range md {
k = strings.ToLower(k)
// filter own keys
if strings.HasPrefix(k, "x-api-field-") {
matches[strings.TrimPrefix(k, "x-api-field-")] = v
delete(md, k)
} else if k == "x-api-body" {
bodydst = v
delete(md, k)
}
}
// map of all fields
req := make(map[string]interface{}, len(md))
// get fields from url values
if len(r.URL.RawQuery) > 0 {
umd := make(map[string]interface{})
err = qson.Unmarshal(&umd, r.URL.RawQuery)
if err != nil {
return nil, err
}
for k, v := range umd {
matches[k] = v
}
}
// restore context without fields
*r = *r.Clone(metadata.NewContext(ctx, md))
for k, v := range matches {
ps := strings.Split(k, ".")
if len(ps) == 1 {
req[k] = v
continue
}
em := make(map[string]interface{})
em[ps[len(ps)-1]] = v
for i := len(ps) - 2; i > 0; i-- {
nm := make(map[string]interface{})
nm[ps[i]] = em
em = nm
}
if vm, ok := req[ps[0]]; ok {
// nested map
nm := vm.(map[string]interface{})
for vk, vv := range em {
nm[vk] = vv
}
req[ps[0]] = nm
} else {
req[ps[0]] = em
}
}
pathbuf := []byte("{}")
if len(req) > 0 {
pathbuf, err = json.Marshal(req)
if err != nil {
return nil, err
}
}
urlbuf := []byte("{}")
out, err := jsonpatch.MergeMergePatches(urlbuf, pathbuf)
if err != nil {
return nil, err
}
switch r.Method {
case "GET":
// empty response
if strings.Contains(ct, "application/json") && string(out) == "{}" {
return out, nil
} else if string(out) == "{}" && !strings.Contains(ct, "application/json") {
return []byte{}, nil
}
return out, nil
case "PATCH", "POST", "PUT", "DELETE":
bodybuf := []byte("{}")
buf := bufferPool.Get()
defer bufferPool.Put(buf)
if _, err := buf.ReadFrom(r.Body); err != nil {
return nil, err
}
if b := buf.Bytes(); len(b) > 0 {
bodybuf = b
}
if bodydst == "" || bodydst == "*" {
if out, err = jsonpatch.MergeMergePatches(out, bodybuf); err == nil {
return out, nil
}
}
var jsonbody map[string]interface{}
if json.Valid(bodybuf) {
if err = json.Unmarshal(bodybuf, &jsonbody); err != nil {
return nil, err
}
}
dstmap := make(map[string]interface{})
ps := strings.Split(bodydst, ".")
if len(ps) == 1 {
if jsonbody != nil {
dstmap[ps[0]] = jsonbody
} else {
// old unexpected behaviour
dstmap[ps[0]] = bodybuf
}
} else {
em := make(map[string]interface{})
if jsonbody != nil {
em[ps[len(ps)-1]] = jsonbody
} else {
// old unexpected behaviour
em[ps[len(ps)-1]] = bodybuf
}
for i := len(ps) - 2; i > 0; i-- {
nm := make(map[string]interface{})
nm[ps[i]] = em
em = nm
}
dstmap[ps[0]] = em
}
bodyout, err := json.Marshal(dstmap)
if err != nil {
return nil, err
}
if out, err = jsonpatch.MergeMergePatches(out, bodyout); err == nil {
return out, nil
}
//fallback to previous unknown behaviour
return bodybuf, nil
}
return []byte{}, nil
}
func writeError(w http.ResponseWriter, r *http.Request, err error) {
ce := errors.Parse(err.Error())
switch ce.Code {
case 0:
// assuming it's totally screwed
ce.Code = 500
ce.Id = "go.micro.api"
ce.Status = http.StatusText(500)
ce.Detail = "error during request: " + ce.Detail
w.WriteHeader(500)
default:
w.WriteHeader(int(ce.Code))
}
// response content type
w.Header().Set("Content-Type", "application/json")
// Set trailers
if strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
w.Header().Set("Trailer", "grpc-status")
w.Header().Set("Trailer", "grpc-message")
w.Header().Set("grpc-status", "13")
w.Header().Set("grpc-message", ce.Detail)
}
_, werr := w.Write([]byte(ce.Error()))
if werr != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(werr)
}
}
}
func writeResponse(w http.ResponseWriter, r *http.Request, rsp []byte) {
w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
w.Header().Set("Content-Length", strconv.Itoa(len(rsp)))
// Set trailers
if strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
w.Header().Set("Trailer", "grpc-status")
w.Header().Set("Trailer", "grpc-message")
w.Header().Set("grpc-status", "0")
w.Header().Set("grpc-message", "")
}
// write 204 status if rsp is nil
if len(rsp) == 0 {
w.WriteHeader(http.StatusNoContent)
}
// write response
_, err := w.Write(rsp)
if err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(err)
}
}
}
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &rpcHandler{
opts: options,
}
}
func WithService(s *api.Service, opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &rpcHandler{
opts: options,
s: s,
}
}

View File

@@ -1,112 +0,0 @@
package rpc
import (
"bytes"
"encoding/json"
"net/http"
"testing"
"github.com/golang/protobuf/proto"
go_api "github.com/micro/go-micro/v3/api/proto"
)
func TestRequestPayloadFromRequest(t *testing.T) {
// our test event so that we can validate serialising / deserializing of true protos works
protoEvent := go_api.Event{
Name: "Test",
}
protoBytes, err := proto.Marshal(&protoEvent)
if err != nil {
t.Fatal("Failed to marshal proto", err)
}
jsonBytes, err := json.Marshal(protoEvent)
if err != nil {
t.Fatal("Failed to marshal proto to JSON ", err)
}
jsonUrlBytes := []byte(`{"key1":"val1","key2":"val2","name":"Test"}`)
t.Run("extracting a json from a POST request with url params", func(t *testing.T) {
r, err := http.NewRequest("POST", "http://localhost/my/path?key1=val1&key2=val2", bytes.NewReader(jsonBytes))
if err != nil {
t.Fatalf("Failed to created http.Request: %v", err)
}
extByte, err := requestPayload(r)
if err != nil {
t.Fatalf("Failed to extract payload from request: %v", err)
}
if string(extByte) != string(jsonUrlBytes) {
t.Fatalf("Expected %v and %v to match", string(extByte), jsonUrlBytes)
}
})
t.Run("extracting a proto from a POST request", func(t *testing.T) {
r, err := http.NewRequest("POST", "http://localhost/my/path", bytes.NewReader(protoBytes))
if err != nil {
t.Fatalf("Failed to created http.Request: %v", err)
}
extByte, err := requestPayload(r)
if err != nil {
t.Fatalf("Failed to extract payload from request: %v", err)
}
if string(extByte) != string(protoBytes) {
t.Fatalf("Expected %v and %v to match", string(extByte), string(protoBytes))
}
})
t.Run("extracting JSON from a POST request", func(t *testing.T) {
r, err := http.NewRequest("POST", "http://localhost/my/path", bytes.NewReader(jsonBytes))
if err != nil {
t.Fatalf("Failed to created http.Request: %v", err)
}
extByte, err := requestPayload(r)
if err != nil {
t.Fatalf("Failed to extract payload from request: %v", err)
}
if string(extByte) != string(jsonBytes) {
t.Fatalf("Expected %v and %v to match", string(extByte), string(jsonBytes))
}
})
t.Run("extracting params from a GET request", func(t *testing.T) {
r, err := http.NewRequest("GET", "http://localhost/my/path", nil)
if err != nil {
t.Fatalf("Failed to created http.Request: %v", err)
}
q := r.URL.Query()
q.Add("name", "Test")
r.URL.RawQuery = q.Encode()
extByte, err := requestPayload(r)
if err != nil {
t.Fatalf("Failed to extract payload from request: %v", err)
}
if string(extByte) != string(jsonBytes) {
t.Fatalf("Expected %v and %v to match", string(extByte), string(jsonBytes))
}
})
t.Run("GET request with no params", func(t *testing.T) {
r, err := http.NewRequest("GET", "http://localhost/my/path", nil)
if err != nil {
t.Fatalf("Failed to created http.Request: %v", err)
}
extByte, err := requestPayload(r)
if err != nil {
t.Fatalf("Failed to extract payload from request: %v", err)
}
if string(extByte) != "" {
t.Fatalf("Expected %v and %v to match", string(extByte), "")
}
})
}

View File

@@ -1,261 +0,0 @@
package rpc
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"strings"
"time"
"github.com/gobwas/httphead"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
"github.com/micro/go-micro/v3/api"
"github.com/micro/go-micro/v3/client"
raw "github.com/micro/go-micro/v3/codec/bytes"
"github.com/micro/go-micro/v3/logger"
"github.com/micro/go-micro/v3/util/router"
)
// serveWebsocket will stream rpc back over websockets assuming json
func serveWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, service *api.Service, c client.Client) {
var op ws.OpCode
ct := r.Header.Get("Content-Type")
// Strip charset from Content-Type (like `application/json; charset=UTF-8`)
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
ct = ct[:idx]
}
// check proto from request
switch ct {
case "application/json":
op = ws.OpText
default:
op = ws.OpBinary
}
hdr := make(http.Header)
if proto, ok := r.Header["Sec-WebSocket-Protocol"]; ok {
for _, p := range proto {
switch p {
case "binary":
hdr["Sec-WebSocket-Protocol"] = []string{"binary"}
op = ws.OpBinary
}
}
}
payload, err := requestPayload(r)
if err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(err)
}
return
}
upgrader := ws.HTTPUpgrader{Timeout: 5 * time.Second,
Protocol: func(proto string) bool {
if strings.Contains(proto, "binary") {
return true
}
// fallback to support all protocols now
return true
},
Extension: func(httphead.Option) bool {
// disable extensions for compatibility
return false
},
Header: hdr,
}
conn, rw, _, err := upgrader.Upgrade(r, w)
if err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(err)
}
return
}
defer func() {
if err := conn.Close(); err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(err)
}
return
}
}()
var request interface{}
if !bytes.Equal(payload, []byte(`{}`)) {
switch ct {
case "application/json", "":
m := json.RawMessage(payload)
request = &m
default:
request = &raw.Frame{Data: payload}
}
}
// we always need to set content type for message
if ct == "" {
ct = "application/json"
}
req := c.NewRequest(
service.Name,
service.Endpoint.Name,
request,
client.WithContentType(ct),
client.StreamingRequest(),
)
// create custom router
callOpt := client.WithRouter(router.New(service.Services))
// create a new stream
stream, err := c.Stream(ctx, req, callOpt)
if err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(err)
}
return
}
if request != nil {
if err = stream.Send(request); err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(err)
}
return
}
}
go writeLoop(rw, stream)
rsp := stream.Response()
// receive from stream and send to client
for {
select {
case <-ctx.Done():
return
case <-stream.Context().Done():
return
default:
// read backend response body
buf, err := rsp.Read()
if err != nil {
// wants to avoid import grpc/status.Status
if strings.Contains(err.Error(), "context canceled") {
return
}
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(err)
}
return
}
// write the response
if err := wsutil.WriteServerMessage(rw, op, buf); err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(err)
}
return
}
if err = rw.Flush(); err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(err)
}
return
}
}
}
}
// writeLoop
func writeLoop(rw io.ReadWriter, stream client.Stream) {
// close stream when done
defer stream.Close()
for {
select {
case <-stream.Context().Done():
return
default:
buf, op, err := wsutil.ReadClientData(rw)
if err != nil {
if wserr, ok := err.(wsutil.ClosedError); ok {
switch wserr.Code {
case ws.StatusGoingAway:
// this happens when user leave the page
return
case ws.StatusNormalClosure, ws.StatusNoStatusRcvd:
// this happens when user close ws connection, or we don't get any status
return
}
}
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(err)
}
return
}
switch op {
default:
// not relevant
continue
case ws.OpText, ws.OpBinary:
break
}
// send to backend
// default to trying json
// if the extracted payload isn't empty lets use it
request := &raw.Frame{Data: buf}
if err := stream.Send(request); err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(err)
}
return
}
}
}
}
func isStream(r *http.Request, srv *api.Service) bool {
// check if it's a web socket
if !isWebSocket(r) {
return false
}
// check if the endpoint supports streaming
for _, service := range srv.Services {
for _, ep := range service.Endpoints {
// skip if it doesn't match the name
if ep.Name != srv.Endpoint.Name {
continue
}
// matched if the name
if v := ep.Metadata["stream"]; v == "true" {
return true
}
}
}
return false
}
func isWebSocket(r *http.Request) bool {
contains := func(key, val string) bool {
vv := strings.Split(r.Header.Get(key), ",")
for _, v := range vv {
if val == strings.ToLower(strings.TrimSpace(v)) {
return true
}
}
return false
}
if contains("Connection", "upgrade") && contains("Upgrade", "websocket") {
return true
}
return false
}

View File

@@ -1,181 +0,0 @@
// Package web contains the web handler including websocket support
package web
import (
"errors"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/micro/go-micro/v3/api"
"github.com/micro/go-micro/v3/api/handler"
"github.com/micro/go-micro/v3/registry"
)
const (
Handler = "web"
)
type webHandler struct {
opts handler.Options
s *api.Service
}
func (wh *webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
service, err := wh.getService(r)
if err != nil {
w.WriteHeader(500)
return
}
if len(service) == 0 {
w.WriteHeader(404)
return
}
rp, err := url.Parse(service)
if err != nil {
w.WriteHeader(500)
return
}
if isWebSocket(r) {
wh.serveWebSocket(rp.Host, w, r)
return
}
httputil.NewSingleHostReverseProxy(rp).ServeHTTP(w, r)
}
// getService returns the service for this request from the selector
func (wh *webHandler) getService(r *http.Request) (string, error) {
var service *api.Service
if wh.s != nil {
// we were given the service
service = wh.s
} else if wh.opts.Router != nil {
// try get service from router
s, err := wh.opts.Router.Route(r)
if err != nil {
return "", err
}
service = s
} else {
// we have no way of routing the request
return "", errors.New("no route found")
}
// get the nodes
var nodes []*registry.Node
for _, srv := range service.Services {
nodes = append(nodes, srv.Nodes...)
}
if len(nodes) == 0 {
return "", errors.New("no route found")
}
// select a random node
node := nodes[rand.Int()%len(nodes)]
return fmt.Sprintf("http://%s", node.Address), nil
}
// serveWebSocket used to serve a web socket proxied connection
func (wh *webHandler) serveWebSocket(host string, w http.ResponseWriter, r *http.Request) {
req := new(http.Request)
*req = *r
if len(host) == 0 {
http.Error(w, "invalid host", 500)
return
}
// set x-forward-for
if clientIP, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
if ips, ok := req.Header["X-Forwarded-For"]; ok {
clientIP = strings.Join(ips, ", ") + ", " + clientIP
}
req.Header.Set("X-Forwarded-For", clientIP)
}
// connect to the backend host
conn, err := net.Dial("tcp", host)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// hijack the connection
hj, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "failed to connect", 500)
return
}
nc, _, err := hj.Hijack()
if err != nil {
return
}
defer nc.Close()
defer conn.Close()
if err = req.Write(conn); err != nil {
return
}
errCh := make(chan error, 2)
cp := func(dst io.Writer, src io.Reader) {
_, err := io.Copy(dst, src)
errCh <- err
}
go cp(conn, nc)
go cp(nc, conn)
<-errCh
}
func isWebSocket(r *http.Request) bool {
contains := func(key, val string) bool {
vv := strings.Split(r.Header.Get(key), ",")
for _, v := range vv {
if val == strings.ToLower(strings.TrimSpace(v)) {
return true
}
}
return false
}
if contains("Connection", "upgrade") && contains("Upgrade", "websocket") {
return true
}
return false
}
func (wh *webHandler) String() string {
return "web"
}
func NewHandler(opts ...handler.Option) handler.Handler {
return &webHandler{
opts: handler.NewOptions(opts...),
}
}
func WithService(s *api.Service, opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &webHandler{
opts: options,
s: s,
}
}

View File

@@ -1,28 +0,0 @@
package proto
type Message struct {
data []byte
}
func (m *Message) ProtoMessage() {}
func (m *Message) Reset() {
*m = Message{}
}
func (m *Message) String() string {
return string(m.data)
}
func (m *Message) Marshal() ([]byte, error) {
return m.data, nil
}
func (m *Message) Unmarshal(data []byte) error {
m.data = data
return nil
}
func NewMessage(data []byte) *Message {
return &Message{data}
}

View File

@@ -1,335 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: api/proto/api.proto
package go_api
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
type Pair struct {
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Values []string `protobuf:"bytes,2,rep,name=values,proto3" json:"values,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Pair) Reset() { *m = Pair{} }
func (m *Pair) String() string { return proto.CompactTextString(m) }
func (*Pair) ProtoMessage() {}
func (*Pair) Descriptor() ([]byte, []int) {
return fileDescriptor_2df576b66d12087a, []int{0}
}
func (m *Pair) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Pair.Unmarshal(m, b)
}
func (m *Pair) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Pair.Marshal(b, m, deterministic)
}
func (m *Pair) XXX_Merge(src proto.Message) {
xxx_messageInfo_Pair.Merge(m, src)
}
func (m *Pair) XXX_Size() int {
return xxx_messageInfo_Pair.Size(m)
}
func (m *Pair) XXX_DiscardUnknown() {
xxx_messageInfo_Pair.DiscardUnknown(m)
}
var xxx_messageInfo_Pair proto.InternalMessageInfo
func (m *Pair) GetKey() string {
if m != nil {
return m.Key
}
return ""
}
func (m *Pair) GetValues() []string {
if m != nil {
return m.Values
}
return nil
}
// A HTTP request as RPC
// Forward by the api handler
type Request struct {
Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"`
Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"`
Header map[string]*Pair `protobuf:"bytes,3,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Get map[string]*Pair `protobuf:"bytes,4,rep,name=get,proto3" json:"get,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Post map[string]*Pair `protobuf:"bytes,5,rep,name=post,proto3" json:"post,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Body string `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
Url string `protobuf:"bytes,7,opt,name=url,proto3" json:"url,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Request) Reset() { *m = Request{} }
func (m *Request) String() string { return proto.CompactTextString(m) }
func (*Request) ProtoMessage() {}
func (*Request) Descriptor() ([]byte, []int) {
return fileDescriptor_2df576b66d12087a, []int{1}
}
func (m *Request) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Request.Unmarshal(m, b)
}
func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Request.Marshal(b, m, deterministic)
}
func (m *Request) XXX_Merge(src proto.Message) {
xxx_messageInfo_Request.Merge(m, src)
}
func (m *Request) XXX_Size() int {
return xxx_messageInfo_Request.Size(m)
}
func (m *Request) XXX_DiscardUnknown() {
xxx_messageInfo_Request.DiscardUnknown(m)
}
var xxx_messageInfo_Request proto.InternalMessageInfo
func (m *Request) GetMethod() string {
if m != nil {
return m.Method
}
return ""
}
func (m *Request) GetPath() string {
if m != nil {
return m.Path
}
return ""
}
func (m *Request) GetHeader() map[string]*Pair {
if m != nil {
return m.Header
}
return nil
}
func (m *Request) GetGet() map[string]*Pair {
if m != nil {
return m.Get
}
return nil
}
func (m *Request) GetPost() map[string]*Pair {
if m != nil {
return m.Post
}
return nil
}
func (m *Request) GetBody() string {
if m != nil {
return m.Body
}
return ""
}
func (m *Request) GetUrl() string {
if m != nil {
return m.Url
}
return ""
}
// A HTTP response as RPC
// Expected response for the api handler
type Response struct {
StatusCode int32 `protobuf:"varint,1,opt,name=statusCode,proto3" json:"statusCode,omitempty"`
Header map[string]*Pair `protobuf:"bytes,2,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Body string `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Response) Reset() { *m = Response{} }
func (m *Response) String() string { return proto.CompactTextString(m) }
func (*Response) ProtoMessage() {}
func (*Response) Descriptor() ([]byte, []int) {
return fileDescriptor_2df576b66d12087a, []int{2}
}
func (m *Response) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Response.Unmarshal(m, b)
}
func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Response.Marshal(b, m, deterministic)
}
func (m *Response) XXX_Merge(src proto.Message) {
xxx_messageInfo_Response.Merge(m, src)
}
func (m *Response) XXX_Size() int {
return xxx_messageInfo_Response.Size(m)
}
func (m *Response) XXX_DiscardUnknown() {
xxx_messageInfo_Response.DiscardUnknown(m)
}
var xxx_messageInfo_Response proto.InternalMessageInfo
func (m *Response) GetStatusCode() int32 {
if m != nil {
return m.StatusCode
}
return 0
}
func (m *Response) GetHeader() map[string]*Pair {
if m != nil {
return m.Header
}
return nil
}
func (m *Response) GetBody() string {
if m != nil {
return m.Body
}
return ""
}
// A HTTP event as RPC
// Forwarded by the event handler
type Event struct {
// e.g login
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// uuid
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
// unix timestamp of event
Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
// event headers
Header map[string]*Pair `protobuf:"bytes,4,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
// the event data
Data string `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Event) Reset() { *m = Event{} }
func (m *Event) String() string { return proto.CompactTextString(m) }
func (*Event) ProtoMessage() {}
func (*Event) Descriptor() ([]byte, []int) {
return fileDescriptor_2df576b66d12087a, []int{3}
}
func (m *Event) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Event.Unmarshal(m, b)
}
func (m *Event) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Event.Marshal(b, m, deterministic)
}
func (m *Event) XXX_Merge(src proto.Message) {
xxx_messageInfo_Event.Merge(m, src)
}
func (m *Event) XXX_Size() int {
return xxx_messageInfo_Event.Size(m)
}
func (m *Event) XXX_DiscardUnknown() {
xxx_messageInfo_Event.DiscardUnknown(m)
}
var xxx_messageInfo_Event proto.InternalMessageInfo
func (m *Event) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *Event) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *Event) GetTimestamp() int64 {
if m != nil {
return m.Timestamp
}
return 0
}
func (m *Event) GetHeader() map[string]*Pair {
if m != nil {
return m.Header
}
return nil
}
func (m *Event) GetData() string {
if m != nil {
return m.Data
}
return ""
}
func init() {
proto.RegisterType((*Pair)(nil), "go.api.Pair")
proto.RegisterType((*Request)(nil), "go.api.Request")
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Request.GetEntry")
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Request.HeaderEntry")
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Request.PostEntry")
proto.RegisterType((*Response)(nil), "go.api.Response")
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Response.HeaderEntry")
proto.RegisterType((*Event)(nil), "go.api.Event")
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Event.HeaderEntry")
}
func init() { proto.RegisterFile("api/proto/api.proto", fileDescriptor_2df576b66d12087a) }
var fileDescriptor_2df576b66d12087a = []byte{
// 393 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x53, 0xcd, 0xce, 0xd3, 0x30,
0x10, 0x54, 0xe2, 0x24, 0x6d, 0xb6, 0x08, 0x21, 0x23, 0x21, 0x53, 0x2a, 0x54, 0xe5, 0x54, 0x21,
0x91, 0x42, 0xcb, 0x01, 0x71, 0x85, 0xaa, 0x1c, 0x2b, 0xbf, 0x81, 0xab, 0x58, 0x6d, 0x44, 0x13,
0x9b, 0xd8, 0xa9, 0xd4, 0x87, 0xe3, 0xc0, 0x63, 0xf0, 0x36, 0xc8, 0x1b, 0xf7, 0xe7, 0xab, 0xfa,
0x5d, 0xbe, 0xaf, 0xb7, 0x89, 0x3d, 0x3b, 0x3b, 0x3b, 0xeb, 0xc0, 0x6b, 0xa1, 0xcb, 0xa9, 0x6e,
0x94, 0x55, 0x53, 0xa1, 0xcb, 0x1c, 0x11, 0x4d, 0x36, 0x2a, 0x17, 0xba, 0xcc, 0x3e, 0x41, 0xb4,
0x12, 0x65, 0x43, 0x5f, 0x01, 0xf9, 0x25, 0x0f, 0x2c, 0x18, 0x07, 0x93, 0x94, 0x3b, 0x48, 0xdf,
0x40, 0xb2, 0x17, 0xbb, 0x56, 0x1a, 0x16, 0x8e, 0xc9, 0x24, 0xe5, 0xfe, 0x2b, 0xfb, 0x4b, 0xa0,
0xc7, 0xe5, 0xef, 0x56, 0x1a, 0xeb, 0x38, 0x95, 0xb4, 0x5b, 0x55, 0xf8, 0x42, 0xff, 0x45, 0x29,
0x44, 0x5a, 0xd8, 0x2d, 0x0b, 0xf1, 0x14, 0x31, 0x9d, 0x43, 0xb2, 0x95, 0xa2, 0x90, 0x0d, 0x23,
0x63, 0x32, 0x19, 0xcc, 0xde, 0xe5, 0x9d, 0x85, 0xdc, 0x8b, 0xe5, 0x3f, 0xf1, 0x76, 0x51, 0xdb,
0xe6, 0xc0, 0x3d, 0x95, 0x7e, 0x00, 0xb2, 0x91, 0x96, 0x45, 0x58, 0xc1, 0xae, 0x2b, 0x96, 0xd2,
0x76, 0x74, 0x47, 0xa2, 0x1f, 0x21, 0xd2, 0xca, 0x58, 0x16, 0x23, 0xf9, 0xed, 0x35, 0x79, 0xa5,
0x8c, 0x67, 0x23, 0xcd, 0x79, 0x5c, 0xab, 0xe2, 0xc0, 0x92, 0xce, 0xa3, 0xc3, 0x2e, 0x85, 0xb6,
0xd9, 0xb1, 0x5e, 0x97, 0x42, 0xdb, 0xec, 0x86, 0x4b, 0x18, 0x5c, 0xf8, 0xba, 0x11, 0x53, 0x06,
0x31, 0x06, 0x83, 0xb3, 0x0e, 0x66, 0x2f, 0x8e, 0x6d, 0x5d, 0xaa, 0xbc, 0xbb, 0xfa, 0x16, 0x7e,
0x0d, 0x86, 0x3f, 0xa0, 0x7f, 0xb4, 0xfb, 0x0c, 0x95, 0x05, 0xa4, 0xa7, 0x39, 0x9e, 0x2e, 0x93,
0xfd, 0x09, 0xa0, 0xcf, 0xa5, 0xd1, 0xaa, 0x36, 0x92, 0xbe, 0x07, 0x30, 0x56, 0xd8, 0xd6, 0x7c,
0x57, 0x85, 0x44, 0xb5, 0x98, 0x5f, 0x9c, 0xd0, 0x2f, 0xa7, 0xc5, 0x85, 0x98, 0xec, 0xe8, 0x9c,
0x6c, 0xa7, 0x70, 0x73, 0x73, 0xc7, 0x78, 0xc9, 0x39, 0xde, 0xbb, 0x85, 0x99, 0xfd, 0x0b, 0x20,
0x5e, 0xec, 0x65, 0x8d, 0x5b, 0xac, 0x45, 0x25, 0xbd, 0x08, 0x62, 0xfa, 0x12, 0xc2, 0xb2, 0xf0,
0x6f, 0x2f, 0x2c, 0x0b, 0x3a, 0x82, 0xd4, 0x96, 0x95, 0x34, 0x56, 0x54, 0x1a, 0xfd, 0x10, 0x7e,
0x3e, 0xa0, 0x9f, 0x4f, 0xe3, 0x45, 0x0f, 0x1f, 0x0e, 0x36, 0x78, 0x6c, 0xb6, 0x42, 0x58, 0xc1,
0xe2, 0xae, 0xa9, 0xc3, 0x77, 0x9b, 0x6d, 0x9d, 0xe0, 0x0f, 0x3a, 0xff, 0x1f, 0x00, 0x00, 0xff,
0xff, 0xd4, 0x6d, 0x70, 0x51, 0xb7, 0x03, 0x00, 0x00,
}

View File

@@ -1,21 +0,0 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: api/proto/api.proto
package go_api
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package

View File

@@ -1,43 +0,0 @@
syntax = "proto3";
package go.api;
message Pair {
string key = 1;
repeated string values = 2;
}
// A HTTP request as RPC
// Forward by the api handler
message Request {
string method = 1;
string path = 2;
map<string, Pair> header = 3;
map<string, Pair> get = 4;
map<string, Pair> post = 5;
string body = 6; // raw request body; if not application/x-www-form-urlencoded
string url = 7;
}
// A HTTP response as RPC
// Expected response for the api handler
message Response {
int32 statusCode = 1;
map<string, Pair> header = 2;
string body = 3;
}
// A HTTP event as RPC
// Forwarded by the event handler
message Event {
// e.g login
string name = 1;
// uuid
string id = 2;
// unix timestamp of event
int64 timestamp = 3;
// event headers
map<string, Pair> header = 4;
// the event data
string data = 5;
}

View File

@@ -6,13 +6,15 @@ import (
"net/http"
"strings"
"github.com/micro/go-micro/v3/api/resolver"
"github.com/unistack-org/micro/v3/api/resolver"
)
// Resolver struct
type Resolver struct {
opts resolver.Options
}
// Resolve func to resolve enndpoint
func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
// parse options
options := resolver.NewResolveOptions(opts...)
@@ -39,6 +41,7 @@ func (r *Resolver) String() string {
return "grpc"
}
// NewResolver is used to create new Resolver
func NewResolver(opts ...resolver.Option) resolver.Resolver {
return &Resolver{opts: resolver.NewOptions(opts...)}
}

View File

@@ -4,14 +4,15 @@ package host
import (
"net/http"
"github.com/micro/go-micro/v3/api/resolver"
"github.com/unistack-org/micro/v3/api/resolver"
)
type Resolver struct {
type hostResolver struct {
opts resolver.Options
}
func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
// Resolve endpoint
func (r *hostResolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
// parse options
options := resolver.NewResolveOptions(opts...)
@@ -24,10 +25,11 @@ func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*
}, nil
}
func (r *Resolver) String() string {
func (r *hostResolver) String() string {
return "host"
}
// NewResolver creates new host api resolver
func NewResolver(opts ...resolver.Option) resolver.Resolver {
return &Resolver{opts: resolver.NewOptions(opts...)}
return &hostResolver{opts: resolver.NewOptions(opts...)}
}

View File

@@ -1,14 +1,22 @@
package resolver
import (
"github.com/micro/go-micro/v3/registry"
"context"
"github.com/unistack-org/micro/v3/register"
)
// Options struct
type Options struct {
Handler string
// Context is for external defined options
Context context.Context
// Handler name
Handler string
// ServicePrefix is the prefix
ServicePrefix string
}
// Option func
type Option func(o *Options)
// WithHandler sets the handler being used
@@ -27,7 +35,9 @@ func WithServicePrefix(p string) Option {
// NewOptions returns new initialised options
func NewOptions(opts ...Option) Options {
var options Options
options := Options{
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
@@ -51,13 +61,10 @@ func Domain(n string) ResolveOption {
// NewResolveOptions returns new initialised resolve options
func NewResolveOptions(opts ...ResolveOption) ResolveOptions {
var options ResolveOptions
options := ResolveOptions{Domain: register.DefaultDomain}
for _, o := range opts {
o(&options)
}
if len(options.Domain) == 0 {
options.Domain = registry.DefaultDomain
}
return options
}

View File

@@ -5,13 +5,15 @@ import (
"net/http"
"strings"
"github.com/micro/go-micro/v3/api/resolver"
"github.com/unistack-org/micro/v3/api/resolver"
)
// Resolver the path resolver
type Resolver struct {
opts resolver.Options
}
// Resolve resolves endpoint
func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
// parse options
options := resolver.NewResolveOptions(opts...)
@@ -31,10 +33,12 @@ func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*
}, nil
}
// String retruns the string representation
func (r *Resolver) String() string {
return "path"
}
// NewResolver returns new path resolver
func NewResolver(opts ...resolver.Option) resolver.Resolver {
return &Resolver{opts: resolver.NewOptions(opts...)}
}

View File

@@ -7,7 +7,9 @@ import (
)
var (
ErrNotFound = errors.New("not found")
// ErrNotFound returned when endpoint is not found
ErrNotFound = errors.New("not found")
// ErrInvalidPath returned on invalid path
ErrInvalidPath = errors.New("invalid path")
)
@@ -19,7 +21,7 @@ type Resolver interface {
// Endpoint is the endpoint for a http request
type Endpoint struct {
// e.g greeter
// Endpoint name e.g greeter
Name string
// HTTP Host e.g example.com
Host string

View File

@@ -7,22 +7,24 @@ import (
"net/http"
"strings"
"github.com/micro/go-micro/v3/api/resolver"
"github.com/micro/go-micro/v3/logger"
"github.com/unistack-org/micro/v3/api/resolver"
"github.com/unistack-org/micro/v3/logger"
"golang.org/x/net/publicsuffix"
)
// NewResolver creates new subdomain api resolver
func NewResolver(parent resolver.Resolver, opts ...resolver.Option) resolver.Resolver {
options := resolver.NewOptions(opts...)
return &Resolver{options, parent}
return &subdomainResolver{options, parent}
}
type Resolver struct {
type subdomainResolver struct {
opts resolver.Options
resolver.Resolver
}
func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
// Resolve resolve endpoint based on subdomain
func (r *subdomainResolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
if dom := r.Domain(req); len(dom) > 0 {
opts = append(opts, resolver.Domain(dom))
}
@@ -30,7 +32,8 @@ func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*
return r.Resolver.Resolve(req, opts...)
}
func (r *Resolver) Domain(req *http.Request) string {
// Domain returns domain
func (r *subdomainResolver) Domain(req *http.Request) string {
// determine the host, e.g. foobar.m3o.app
host := req.URL.Hostname()
if len(host) == 0 {
@@ -46,7 +49,7 @@ func (r *Resolver) Domain(req *http.Request) string {
return ""
}
// check for dev enviroment
// check for dev environment
if host == "localhost" || host == "127.0.0.1" {
return ""
}
@@ -54,7 +57,9 @@ func (r *Resolver) Domain(req *http.Request) string {
// extract the top level domain plus one (e.g. 'myapp.com')
domain, err := publicsuffix.EffectiveTLDPlusOne(host)
if err != nil {
logger.Debugf("Unable to extract domain from %v", host)
if logger.V(logger.DebugLevel) {
logger.Debug(r.opts.Context, "Unable to extract domain from %v", host)
}
return ""
}
@@ -80,6 +85,6 @@ func (r *Resolver) Domain(req *http.Request) string {
return strings.Join(comps, "-")
}
func (r *Resolver) String() string {
func (r *subdomainResolver) String() string {
return "subdomain"
}

View File

@@ -5,9 +5,7 @@ import (
"net/url"
"testing"
"github.com/micro/go-micro/v3/api/resolver/vpath"
"github.com/stretchr/testify/assert"
"github.com/unistack-org/micro/v3/api/resolver/vpath"
)
func TestResolve(t *testing.T) {
@@ -62,9 +60,13 @@ func TestResolve(t *testing.T) {
t.Run(tc.Name, func(t *testing.T) {
r := NewResolver(vpath.NewResolver())
result, err := r.Resolve(&http.Request{URL: &url.URL{Host: tc.Host, Path: "foo/bar"}})
assert.Nil(t, err, "Expecter err to be nil")
if err != nil {
t.Fatal(err)
}
if result != nil {
assert.Equal(t, tc.Result, result.Domain, "Expected %v but got %v", tc.Result, result.Domain)
if tc.Result != result.Domain {
t.Fatalf("Expected %v but got %v", tc.Result, result.Domain)
}
}
})
}

View File

@@ -7,14 +7,15 @@ import (
"regexp"
"strings"
"github.com/micro/go-micro/v3/api/resolver"
"github.com/unistack-org/micro/v3/api/resolver"
)
// NewResolver creates new vpath api resolver
func NewResolver(opts ...resolver.Option) resolver.Resolver {
return &Resolver{opts: resolver.NewOptions(opts...)}
return &vpathResolver{opts: resolver.NewOptions(opts...)}
}
type Resolver struct {
type vpathResolver struct {
opts resolver.Options
}
@@ -22,7 +23,8 @@ var (
re = regexp.MustCompile("^v[0-9]+$")
)
func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
// Resolve endpoint
func (r *vpathResolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*resolver.Endpoint, error) {
if req.URL.Path == "/" {
return nil, errors.New("unknown name")
}
@@ -60,12 +62,12 @@ func (r *Resolver) Resolve(req *http.Request, opts ...resolver.ResolveOption) (*
}, nil
}
func (r *Resolver) String() string {
return "path"
func (r *vpathResolver) String() string {
return "vpath"
}
// withPrefix transforms "foo" into "go.micro.api.foo"
func (r *Resolver) withPrefix(parts ...string) string {
func (r *vpathResolver) withPrefix(parts ...string) string {
p := r.opts.ServicePrefix
if len(p) > 0 {
parts = append([]string{p}, parts...)

View File

@@ -1,24 +1,36 @@
package router
import (
"github.com/micro/go-micro/v3/api/resolver"
"github.com/micro/go-micro/v3/api/resolver/vpath"
"github.com/micro/go-micro/v3/registry"
"github.com/micro/go-micro/v3/registry/mdns"
"context"
"github.com/unistack-org/micro/v3/api/resolver"
"github.com/unistack-org/micro/v3/api/resolver/vpath"
"github.com/unistack-org/micro/v3/logger"
"github.com/unistack-org/micro/v3/register"
)
// Options holds the options for api router
type Options struct {
Handler string
Registry registry.Registry
// Register for service lookup
Register register.Register
// Resolver to use
Resolver resolver.Resolver
// Logger micro logger
Logger logger.Logger
// Context is for external options
Context context.Context
// Handler name
Handler string
}
// Option func signature
type Option func(o *Options)
// NewOptions returns options struct filled by opts
func NewOptions(opts ...Option) Options {
options := Options{
Handler: "meta",
Registry: mdns.NewRegistry(),
Context: context.Background(),
Handler: "meta",
}
for _, o := range opts {
@@ -34,18 +46,28 @@ func NewOptions(opts ...Option) Options {
return options
}
// WithContext sets the context
func WithContext(ctx context.Context) Option {
return func(o *Options) {
o.Context = ctx
}
}
// WithHandler sets the handler
func WithHandler(h string) Option {
return func(o *Options) {
o.Handler = h
}
}
func WithRegistry(r registry.Registry) Option {
// WithRegister sets the register
func WithRegister(r register.Register) Option {
return func(o *Options) {
o.Registry = r
o.Register = r
}
}
// WithResolver sets the resolver
func WithResolver(r resolver.Resolver) Option {
return func(o *Options) {
o.Resolver = r

View File

@@ -1,501 +0,0 @@
// Package registry provides a dynamic api service router
package registry
import (
"errors"
"fmt"
"net/http"
"regexp"
"strings"
"sync"
"time"
"github.com/micro/go-micro/v3/api"
"github.com/micro/go-micro/v3/api/router"
"github.com/micro/go-micro/v3/logger"
"github.com/micro/go-micro/v3/metadata"
"github.com/micro/go-micro/v3/registry"
"github.com/micro/go-micro/v3/registry/cache"
util "github.com/micro/go-micro/v3/util/router"
)
// endpoint struct, that holds compiled pcre
type endpoint struct {
hostregs []*regexp.Regexp
pathregs []util.Pattern
pcreregs []*regexp.Regexp
}
// router is the default router
type registryRouter struct {
exit chan bool
opts router.Options
// registry cache
rc cache.Cache
sync.RWMutex
eps map[string]*api.Service
// compiled regexp for host and path
ceps map[string]*endpoint
}
func (r *registryRouter) isClosed() bool {
select {
case <-r.exit:
return true
default:
return false
}
}
// refresh list of api services
func (r *registryRouter) refresh() {
var attempts int
for {
services, err := r.opts.Registry.ListServices()
if err != nil {
attempts++
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("unable to list services: %v", err)
}
time.Sleep(time.Duration(attempts) * time.Second)
continue
}
attempts = 0
// for each service, get service and store endpoints
for _, s := range services {
service, err := r.rc.GetService(s.Name)
if err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("unable to get service: %v", err)
}
continue
}
r.store(service)
}
// refresh list in 10 minutes... cruft
// use registry watching
select {
case <-time.After(time.Minute * 10):
case <-r.exit:
return
}
}
}
// process watch event
func (r *registryRouter) process(res *registry.Result) {
// skip these things
if res == nil || res.Service == nil {
return
}
// get entry from cache
service, err := r.rc.GetService(res.Service.Name)
if err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("unable to get %v service: %v", res.Service.Name, err)
}
return
}
// update our local endpoints
r.store(service)
}
// store local endpoint cache
func (r *registryRouter) store(services []*registry.Service) {
// endpoints
eps := map[string]*api.Service{}
// services
names := map[string]bool{}
// create a new endpoint mapping
for _, service := range services {
// set names we need later
names[service.Name] = true
// map per endpoint
for _, sep := range service.Endpoints {
// create a key service:endpoint_name
key := fmt.Sprintf("%s.%s", service.Name, sep.Name)
// decode endpoint
end := api.Decode(sep.Metadata)
// no endpoint or no name
if end == nil || len(end.Name) == 0 {
continue
}
// if we got nothing skip
if err := api.Validate(end); err != nil {
if logger.V(logger.TraceLevel, logger.DefaultLogger) {
logger.Tracef("endpoint validation failed: %v", err)
}
continue
}
// try get endpoint
ep, ok := eps[key]
if !ok {
ep = &api.Service{Name: service.Name}
}
// overwrite the endpoint
ep.Endpoint = end
// append services
ep.Services = append(ep.Services, service)
// store it
eps[key] = ep
}
}
r.Lock()
defer r.Unlock()
// delete any existing eps for services we know
for key, service := range r.eps {
// skip what we don't care about
if !names[service.Name] {
continue
}
// ok we know this thing
// delete delete delete
delete(r.eps, key)
}
// now set the eps we have
for name, ep := range eps {
r.eps[name] = ep
cep := &endpoint{}
for _, h := range ep.Endpoint.Host {
if h == "" || h == "*" {
continue
}
hostreg, err := regexp.CompilePOSIX(h)
if err != nil {
if logger.V(logger.TraceLevel, logger.DefaultLogger) {
logger.Tracef("endpoint have invalid host regexp: %v", err)
}
continue
}
cep.hostregs = append(cep.hostregs, hostreg)
}
for _, p := range ep.Endpoint.Path {
var pcreok bool
if p[0] == '^' && p[len(p)-1] == '$' {
pcrereg, err := regexp.CompilePOSIX(p)
if err == nil {
cep.pcreregs = append(cep.pcreregs, pcrereg)
pcreok = true
}
}
rule, err := util.Parse(p)
if err != nil && !pcreok {
if logger.V(logger.TraceLevel, logger.DefaultLogger) {
logger.Tracef("endpoint have invalid path pattern: %v", err)
}
continue
} else if err != nil && pcreok {
continue
}
tpl := rule.Compile()
pathreg, err := util.NewPattern(tpl.Version, tpl.OpCodes, tpl.Pool, "")
if err != nil {
if logger.V(logger.TraceLevel, logger.DefaultLogger) {
logger.Tracef("endpoint have invalid path pattern: %v", err)
}
continue
}
cep.pathregs = append(cep.pathregs, pathreg)
}
r.ceps[name] = cep
}
}
// watch for endpoint changes
func (r *registryRouter) watch() {
var attempts int
for {
if r.isClosed() {
return
}
// watch for changes
w, err := r.opts.Registry.Watch()
if err != nil {
attempts++
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("error watching endpoints: %v", err)
}
time.Sleep(time.Duration(attempts) * time.Second)
continue
}
ch := make(chan bool)
go func() {
select {
case <-ch:
w.Stop()
case <-r.exit:
w.Stop()
}
}()
// reset if we get here
attempts = 0
for {
// process next event
res, err := w.Next()
if err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("error getting next endpoint: %v", err)
}
close(ch)
break
}
r.process(res)
}
}
}
func (r *registryRouter) Options() router.Options {
return r.opts
}
func (r *registryRouter) Close() error {
select {
case <-r.exit:
return nil
default:
close(r.exit)
r.rc.Stop()
}
return nil
}
func (r *registryRouter) Register(ep *api.Endpoint) error {
return nil
}
func (r *registryRouter) Deregister(ep *api.Endpoint) error {
return nil
}
func (r *registryRouter) Endpoint(req *http.Request) (*api.Service, error) {
if r.isClosed() {
return nil, errors.New("router closed")
}
r.RLock()
defer r.RUnlock()
var idx int
if len(req.URL.Path) > 0 && req.URL.Path != "/" {
idx = 1
}
path := strings.Split(req.URL.Path[idx:], "/")
// use the first match
// TODO: weighted matching
for n, e := range r.eps {
cep, ok := r.ceps[n]
if !ok {
continue
}
ep := e.Endpoint
var mMatch, hMatch, pMatch bool
// 1. try method
for _, m := range ep.Method {
if m == req.Method {
mMatch = true
break
}
}
if !mMatch {
continue
}
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
logger.Debugf("api method match %s", req.Method)
}
// 2. try host
if len(ep.Host) == 0 {
hMatch = true
} else {
for idx, h := range ep.Host {
if h == "" || h == "*" {
hMatch = true
break
} else {
if cep.hostregs[idx].MatchString(req.URL.Host) {
hMatch = true
break
}
}
}
}
if !hMatch {
continue
}
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
logger.Debugf("api host match %s", req.URL.Host)
}
// 3. try path via google.api path matching
for _, pathreg := range cep.pathregs {
matches, err := pathreg.Match(path, "")
if err != nil {
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
logger.Debugf("api gpath not match %s != %v", path, pathreg)
}
continue
}
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
logger.Debugf("api gpath match %s = %v", path, pathreg)
}
pMatch = true
ctx := req.Context()
md, ok := metadata.FromContext(ctx)
if !ok {
md = make(metadata.Metadata)
}
for k, v := range matches {
md[fmt.Sprintf("x-api-field-%s", k)] = v
}
md["x-api-body"] = ep.Body
*req = *req.Clone(metadata.NewContext(ctx, md))
break
}
if !pMatch {
// 4. try path via pcre path matching
for _, pathreg := range cep.pcreregs {
if !pathreg.MatchString(req.URL.Path) {
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
logger.Debugf("api pcre path not match %s != %v", path, pathreg)
}
continue
}
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
logger.Debugf("api pcre path match %s != %v", path, pathreg)
}
pMatch = true
break
}
}
if !pMatch {
continue
}
// TODO: Percentage traffic
// we got here, so its a match
return e, nil
}
// no match
return nil, errors.New("not found")
}
func (r *registryRouter) Route(req *http.Request) (*api.Service, error) {
if r.isClosed() {
return nil, errors.New("router closed")
}
// try get an endpoint
ep, err := r.Endpoint(req)
if err == nil {
return ep, nil
}
// error not nil
// ignore that shit
// TODO: don't ignore that shit
// get the service name
rp, err := r.opts.Resolver.Resolve(req)
if err != nil {
return nil, err
}
// service name
name := rp.Name
// get service
services, err := r.rc.GetService(name, registry.GetDomain(rp.Domain))
if err != nil {
return nil, err
}
// only use endpoint matching when the meta handler is set aka api.Default
switch r.opts.Handler {
// rpc handlers
case "meta", "api", "rpc":
handler := r.opts.Handler
// set default handler to api
if r.opts.Handler == "meta" {
handler = "rpc"
}
// construct api service
return &api.Service{
Name: name,
Endpoint: &api.Endpoint{
Name: rp.Method,
Handler: handler,
},
Services: services,
}, nil
// http handler
case "http", "proxy", "web":
// construct api service
return &api.Service{
Name: name,
Endpoint: &api.Endpoint{
Name: req.URL.String(),
Handler: r.opts.Handler,
Host: []string{req.Host},
Method: []string{req.Method},
Path: []string{req.URL.Path},
},
Services: services,
}, nil
}
return nil, errors.New("unknown handler")
}
func newRouter(opts ...router.Option) *registryRouter {
options := router.NewOptions(opts...)
r := &registryRouter{
exit: make(chan bool),
opts: options,
rc: cache.New(options.Registry),
eps: make(map[string]*api.Service),
ceps: make(map[string]*endpoint),
}
go r.watch()
go r.refresh()
return r
}
// NewRouter returns the default router
func NewRouter(opts ...router.Option) router.Router {
return newRouter(opts...)
}

View File

@@ -1,34 +0,0 @@
package registry
import (
"testing"
"github.com/micro/go-micro/v3/registry"
"github.com/stretchr/testify/assert"
)
func TestStoreRegex(t *testing.T) {
router := newRouter()
router.store([]*registry.Service{
{
Name: "Foobar",
Version: "latest",
Endpoints: []*registry.Endpoint{
{
Name: "foo",
Metadata: map[string]string{
"endpoint": "FooEndpoint",
"description": "Some description",
"method": "POST",
"path": "^/foo/$",
"handler": "rpc",
},
},
},
Metadata: map[string]string{},
},
},
)
assert.Len(t, router.ceps["Foobar.foo"].pcreregs, 1)
}

View File

@@ -4,13 +4,20 @@ package router
import (
"net/http"
"github.com/micro/go-micro/v3/api"
"github.com/unistack-org/micro/v3/api"
)
var (
// DefaultRouter contains default router implementation
DefaultRouter Router
)
// Router is used to determine an endpoint for a request
type Router interface {
// Returns options
Options() Options
// Init initialize router
Init(...Option) error
// Stop the router
Close() error
// Endpoint returns an api.Service endpoint or an error if it does not exist
@@ -21,4 +28,6 @@ type Router interface {
Deregister(ep *api.Endpoint) error
// Route returns an api.Service route
Route(r *http.Request) (*api.Service, error)
// String representation of router
String() string
}

View File

@@ -1,251 +0,0 @@
package router_test
import (
"context"
"fmt"
"io/ioutil"
"log"
"net/http"
"testing"
"time"
"github.com/micro/go-micro/v3/api"
"github.com/micro/go-micro/v3/api/handler"
"github.com/micro/go-micro/v3/api/handler/rpc"
"github.com/micro/go-micro/v3/api/router"
rregistry "github.com/micro/go-micro/v3/api/router/registry"
rstatic "github.com/micro/go-micro/v3/api/router/static"
"github.com/micro/go-micro/v3/client"
gcli "github.com/micro/go-micro/v3/client/grpc"
rmemory "github.com/micro/go-micro/v3/registry/memory"
rt "github.com/micro/go-micro/v3/router"
regRouter "github.com/micro/go-micro/v3/router/registry"
"github.com/micro/go-micro/v3/server"
gsrv "github.com/micro/go-micro/v3/server/grpc"
pb "github.com/micro/go-micro/v3/server/grpc/proto"
)
// server is used to implement helloworld.GreeterServer.
type testServer struct {
msgCount int
}
// TestHello implements helloworld.GreeterServer
func (s *testServer) Call(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
rsp.Msg = "Hello " + req.Uuid
return nil
}
// TestHello implements helloworld.GreeterServer
func (s *testServer) CallPcre(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
rsp.Msg = "Hello " + req.Uuid
return nil
}
// TestHello implements helloworld.GreeterServer
func (s *testServer) CallPcreInvalid(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
rsp.Msg = "Hello " + req.Uuid
return nil
}
func initial(t *testing.T) (server.Server, client.Client) {
r := rmemory.NewRegistry()
// create a new client
s := gsrv.NewServer(
server.Name("foo"),
server.Registry(r),
)
rtr := regRouter.NewRouter(
rt.Registry(r),
)
// create a new server
c := gcli.NewClient(
client.Router(rtr),
)
h := &testServer{}
pb.RegisterTestHandler(s, h)
if err := s.Start(); err != nil {
t.Fatalf("failed to start: %v", err)
}
return s, c
}
func check(t *testing.T, addr string, path string, expected string) {
req, err := http.NewRequest("POST", fmt.Sprintf(path, addr), nil)
if err != nil {
t.Fatalf("Failed to created http.Request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
rsp, err := (&http.Client{}).Do(req)
if err != nil {
t.Fatalf("Failed to created http.Request: %v", err)
}
defer rsp.Body.Close()
buf, err := ioutil.ReadAll(rsp.Body)
if err != nil {
t.Fatal(err)
}
jsonMsg := expected
if string(buf) != jsonMsg {
t.Fatalf("invalid message received, parsing error %s != %s", buf, jsonMsg)
}
}
func TestRouterRegistryPcre(t *testing.T) {
s, c := initial(t)
defer s.Stop()
router := rregistry.NewRouter(
router.WithHandler(rpc.Handler),
router.WithRegistry(s.Options().Registry),
)
hrpc := rpc.NewHandler(
handler.WithClient(c),
handler.WithRouter(router),
)
hsrv := &http.Server{
Handler: hrpc,
Addr: "127.0.0.1:6543",
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
IdleTimeout: 20 * time.Second,
MaxHeaderBytes: 1024 * 1024 * 1, // 1Mb
}
go func() {
log.Println(hsrv.ListenAndServe())
}()
defer hsrv.Close()
time.Sleep(1 * time.Second)
check(t, hsrv.Addr, "http://%s/api/v0/test/call/TEST", `{"msg":"Hello TEST"}`)
}
func TestRouterStaticPcre(t *testing.T) {
s, c := initial(t)
defer s.Stop()
router := rstatic.NewRouter(
router.WithHandler(rpc.Handler),
router.WithRegistry(s.Options().Registry),
)
err := router.Register(&api.Endpoint{
Name: "foo.Test.Call",
Method: []string{"POST"},
Path: []string{"^/api/v0/test/call/?$"},
Handler: "rpc",
})
if err != nil {
t.Fatal(err)
}
hrpc := rpc.NewHandler(
handler.WithClient(c),
handler.WithRouter(router),
)
hsrv := &http.Server{
Handler: hrpc,
Addr: "127.0.0.1:6543",
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
IdleTimeout: 20 * time.Second,
MaxHeaderBytes: 1024 * 1024 * 1, // 1Mb
}
go func() {
log.Println(hsrv.ListenAndServe())
}()
defer hsrv.Close()
time.Sleep(1 * time.Second)
check(t, hsrv.Addr, "http://%s/api/v0/test/call", `{"msg":"Hello "}`)
}
func TestRouterStaticGpath(t *testing.T) {
s, c := initial(t)
defer s.Stop()
router := rstatic.NewRouter(
router.WithHandler(rpc.Handler),
router.WithRegistry(s.Options().Registry),
)
err := router.Register(&api.Endpoint{
Name: "foo.Test.Call",
Method: []string{"POST"},
Path: []string{"/api/v0/test/call/{uuid}"},
Handler: "rpc",
})
if err != nil {
t.Fatal(err)
}
hrpc := rpc.NewHandler(
handler.WithClient(c),
handler.WithRouter(router),
)
hsrv := &http.Server{
Handler: hrpc,
Addr: "127.0.0.1:6543",
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
IdleTimeout: 20 * time.Second,
MaxHeaderBytes: 1024 * 1024 * 1, // 1Mb
}
go func() {
log.Println(hsrv.ListenAndServe())
}()
defer hsrv.Close()
time.Sleep(1 * time.Second)
check(t, hsrv.Addr, "http://%s/api/v0/test/call/TEST", `{"msg":"Hello TEST"}`)
}
func TestRouterStaticPcreInvalid(t *testing.T) {
var ep *api.Endpoint
var err error
s, c := initial(t)
defer s.Stop()
router := rstatic.NewRouter(
router.WithHandler(rpc.Handler),
router.WithRegistry(s.Options().Registry),
)
ep = &api.Endpoint{
Name: "foo.Test.Call",
Method: []string{"POST"},
Path: []string{"^/api/v0/test/call/?"},
Handler: "rpc",
}
err = router.Register(ep)
if err == nil {
t.Fatalf("invalid endpoint %v", ep)
}
ep = &api.Endpoint{
Name: "foo.Test.Call",
Method: []string{"POST"},
Path: []string{"/api/v0/test/call/?$"},
Handler: "rpc",
}
err = router.Register(ep)
if err == nil {
t.Fatalf("invalid endpoint %v", ep)
}
_ = c
}

View File

@@ -1,356 +0,0 @@
package static
import (
"errors"
"fmt"
"net/http"
"regexp"
"strings"
"sync"
"github.com/micro/go-micro/v3/api"
"github.com/micro/go-micro/v3/api/router"
"github.com/micro/go-micro/v3/logger"
"github.com/micro/go-micro/v3/metadata"
"github.com/micro/go-micro/v3/registry"
rutil "github.com/micro/go-micro/v3/util/registry"
util "github.com/micro/go-micro/v3/util/router"
)
type endpoint struct {
apiep *api.Endpoint
hostregs []*regexp.Regexp
pathregs []util.Pattern
pcreregs []*regexp.Regexp
}
// router is the default router
type staticRouter struct {
exit chan bool
opts router.Options
sync.RWMutex
eps map[string]*endpoint
}
func (r *staticRouter) isClosed() bool {
select {
case <-r.exit:
return true
default:
return false
}
}
/*
// watch for endpoint changes
func (r *staticRouter) watch() {
var attempts int
for {
if r.isClosed() {
return
}
// watch for changes
w, err := r.opts.Registry.Watch()
if err != nil {
attempts++
log.Println("Error watching endpoints", err)
time.Sleep(time.Duration(attempts) * time.Second)
continue
}
ch := make(chan bool)
go func() {
select {
case <-ch:
w.Stop()
case <-r.exit:
w.Stop()
}
}()
// reset if we get here
attempts = 0
for {
// process next event
res, err := w.Next()
if err != nil {
log.Println("Error getting next endpoint", err)
close(ch)
break
}
r.process(res)
}
}
}
*/
func (r *staticRouter) Register(ep *api.Endpoint) error {
if err := api.Validate(ep); err != nil {
return err
}
var pathregs []util.Pattern
var hostregs []*regexp.Regexp
var pcreregs []*regexp.Regexp
for _, h := range ep.Host {
if h == "" || h == "*" {
continue
}
hostreg, err := regexp.CompilePOSIX(h)
if err != nil {
return err
}
hostregs = append(hostregs, hostreg)
}
for _, p := range ep.Path {
var pcreok bool
// pcre only when we have start and end markers
if p[0] == '^' && p[len(p)-1] == '$' {
pcrereg, err := regexp.CompilePOSIX(p)
if err == nil {
pcreregs = append(pcreregs, pcrereg)
pcreok = true
}
}
rule, err := util.Parse(p)
if err != nil && !pcreok {
return err
} else if err != nil && pcreok {
continue
}
tpl := rule.Compile()
pathreg, err := util.NewPattern(tpl.Version, tpl.OpCodes, tpl.Pool, "")
if err != nil {
return err
}
pathregs = append(pathregs, pathreg)
}
r.Lock()
r.eps[ep.Name] = &endpoint{
apiep: ep,
pcreregs: pcreregs,
pathregs: pathregs,
hostregs: hostregs,
}
r.Unlock()
return nil
}
func (r *staticRouter) Deregister(ep *api.Endpoint) error {
if err := api.Validate(ep); err != nil {
return err
}
r.Lock()
delete(r.eps, ep.Name)
r.Unlock()
return nil
}
func (r *staticRouter) Options() router.Options {
return r.opts
}
func (r *staticRouter) Close() error {
select {
case <-r.exit:
return nil
default:
close(r.exit)
}
return nil
}
func (r *staticRouter) Endpoint(req *http.Request) (*api.Service, error) {
ep, err := r.endpoint(req)
if err != nil {
return nil, err
}
epf := strings.Split(ep.apiep.Name, ".")
services, err := r.opts.Registry.GetService(epf[0])
if err != nil {
return nil, err
}
// hack for stream endpoint
if ep.apiep.Stream {
svcs := rutil.Copy(services)
for _, svc := range svcs {
if len(svc.Endpoints) == 0 {
e := &registry.Endpoint{}
e.Name = strings.Join(epf[1:], ".")
e.Metadata = make(map[string]string)
e.Metadata["stream"] = "true"
svc.Endpoints = append(svc.Endpoints, e)
}
for _, e := range svc.Endpoints {
e.Name = strings.Join(epf[1:], ".")
e.Metadata = make(map[string]string)
e.Metadata["stream"] = "true"
}
}
services = svcs
}
svc := &api.Service{
Name: epf[0],
Endpoint: &api.Endpoint{
Name: strings.Join(epf[1:], "."),
Handler: "rpc",
Host: ep.apiep.Host,
Method: ep.apiep.Method,
Path: ep.apiep.Path,
Body: ep.apiep.Body,
Stream: ep.apiep.Stream,
},
Services: services,
}
return svc, nil
}
func (r *staticRouter) endpoint(req *http.Request) (*endpoint, error) {
if r.isClosed() {
return nil, errors.New("router closed")
}
r.RLock()
defer r.RUnlock()
var idx int
if len(req.URL.Path) > 0 && req.URL.Path != "/" {
idx = 1
}
path := strings.Split(req.URL.Path[idx:], "/")
// use the first match
// TODO: weighted matching
for _, ep := range r.eps {
var mMatch, hMatch, pMatch bool
// 1. try method
for _, m := range ep.apiep.Method {
if m == req.Method {
mMatch = true
break
}
}
if !mMatch {
continue
}
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
logger.Debugf("api method match %s", req.Method)
}
// 2. try host
if len(ep.apiep.Host) == 0 {
hMatch = true
} else {
for idx, h := range ep.apiep.Host {
if h == "" || h == "*" {
hMatch = true
break
} else {
if ep.hostregs[idx].MatchString(req.URL.Host) {
hMatch = true
break
}
}
}
}
if !hMatch {
continue
}
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
logger.Debugf("api host match %s", req.URL.Host)
}
// 3. try google.api path
for _, pathreg := range ep.pathregs {
matches, err := pathreg.Match(path, "")
if err != nil {
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
logger.Debugf("api gpath not match %s != %v", path, pathreg)
}
continue
}
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
logger.Debugf("api gpath match %s = %v", path, pathreg)
}
pMatch = true
ctx := req.Context()
md, ok := metadata.FromContext(ctx)
if !ok {
md = make(metadata.Metadata)
}
for k, v := range matches {
md[fmt.Sprintf("x-api-field-%s", k)] = v
}
md["x-api-body"] = ep.apiep.Body
*req = *req.Clone(metadata.NewContext(ctx, md))
break
}
if !pMatch {
// 4. try path via pcre path matching
for _, pathreg := range ep.pcreregs {
if !pathreg.MatchString(req.URL.Path) {
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
logger.Debugf("api pcre path not match %s != %v", req.URL.Path, pathreg)
}
continue
}
pMatch = true
break
}
}
if !pMatch {
continue
}
// TODO: Percentage traffic
// we got here, so its a match
return ep, nil
}
// no match
return nil, fmt.Errorf("endpoint not found for %v", req.URL)
}
func (r *staticRouter) Route(req *http.Request) (*api.Service, error) {
if r.isClosed() {
return nil, errors.New("router closed")
}
// try get an endpoint
ep, err := r.Endpoint(req)
if err != nil {
return nil, err
}
return ep, nil
}
func NewRouter(opts ...router.Option) *staticRouter {
options := router.NewOptions(opts...)
r := &staticRouter{
exit: make(chan bool),
opts: options,
eps: make(map[string]*endpoint),
}
//go r.watch()
//go r.refresh()
return r
}

View File

@@ -1,28 +0,0 @@
// Package acme abstracts away various ACME libraries
package acme
import (
"crypto/tls"
"errors"
"net"
)
var (
// ErrProviderNotImplemented can be returned when attempting to
// instantiate an unimplemented provider
ErrProviderNotImplemented = errors.New("Provider not implemented")
)
// Provider is a ACME provider interface
type Provider interface {
// Listen returns a new listener
Listen(...string) (net.Listener, error)
// TLSConfig returns a tls config
TLSConfig(...string) (*tls.Config, error)
}
// The Let's Encrypt ACME endpoints
const (
LetsEncryptStagingCA = "https://acme-staging-v02.api.letsencrypt.org/directory"
LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory"
)

View File

@@ -1,46 +0,0 @@
// Package autocert is the ACME provider from golang.org/x/crypto/acme/autocert
// This provider does not take any config.
package autocert
import (
"crypto/tls"
"net"
"os"
"github.com/micro/go-micro/v3/api/server/acme"
"github.com/micro/go-micro/v3/logger"
"golang.org/x/crypto/acme/autocert"
)
// autoCertACME is the ACME provider from golang.org/x/crypto/acme/autocert
type autocertProvider struct{}
// Listen implements acme.Provider
func (a *autocertProvider) Listen(hosts ...string) (net.Listener, error) {
return autocert.NewListener(hosts...), nil
}
// TLSConfig returns a new tls config
func (a *autocertProvider) TLSConfig(hosts ...string) (*tls.Config, error) {
// create a new manager
m := &autocert.Manager{
Prompt: autocert.AcceptTOS,
}
if len(hosts) > 0 {
m.HostPolicy = autocert.HostWhitelist(hosts...)
}
dir := cacheDir()
if err := os.MkdirAll(dir, 0700); err != nil {
if logger.V(logger.InfoLevel, logger.DefaultLogger) {
logger.Infof("warning: autocert not using a cache: %v", err)
}
} else {
m.Cache = autocert.DirCache(dir)
}
return m.TLSConfig(), nil
}
// New returns an autocert acme.Provider
func NewProvider() acme.Provider {
return &autocertProvider{}
}

View File

@@ -1,16 +0,0 @@
package autocert
import (
"testing"
)
func TestAutocert(t *testing.T) {
l := NewProvider()
if _, ok := l.(*autocertProvider); !ok {
t.Error("NewProvider() didn't return an autocertProvider")
}
// TODO: Travis CI doesn't let us bind :443
// if _, err := l.NewListener(); err != nil {
// t.Error(err.Error())
// }
}

View File

@@ -1,37 +0,0 @@
package autocert
import (
"os"
"path/filepath"
"runtime"
)
func homeDir() string {
if runtime.GOOS == "windows" {
return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
}
if h := os.Getenv("HOME"); h != "" {
return h
}
return "/"
}
func cacheDir() string {
const base = "golang-autocert"
switch runtime.GOOS {
case "darwin":
return filepath.Join(homeDir(), "Library", "Caches", base)
case "windows":
for _, ev := range []string{"APPDATA", "CSIDL_APPDATA", "TEMP", "TMP"} {
if v := os.Getenv(ev); v != "" {
return filepath.Join(v, base)
}
}
// Worst case:
return filepath.Join(homeDir(), base)
}
if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
return filepath.Join(xdg, base)
}
return filepath.Join(homeDir(), ".cache", base)
}

View File

@@ -1,68 +0,0 @@
// Package certmagic is the ACME provider from github.com/caddyserver/certmagic
package certmagic
import (
"crypto/tls"
"math/rand"
"net"
"time"
"github.com/caddyserver/certmagic"
"github.com/micro/go-micro/v3/api/server/acme"
"github.com/micro/go-micro/v3/logger"
)
type certmagicProvider struct {
opts acme.Options
}
// TODO: set self-contained options
func (c *certmagicProvider) setup() {
certmagic.DefaultACME.CA = c.opts.CA
if c.opts.ChallengeProvider != nil {
// Enabling DNS Challenge disables the other challenges
certmagic.DefaultACME.DNSProvider = c.opts.ChallengeProvider
}
if c.opts.OnDemand {
certmagic.Default.OnDemand = new(certmagic.OnDemandConfig)
}
if c.opts.Cache != nil {
// already validated by new()
certmagic.Default.Storage = c.opts.Cache.(certmagic.Storage)
}
// If multiple instances of the provider are running, inject some
// randomness so they don't collide
// RenewalWindowRatio [0.33 - 0.50)
rand.Seed(time.Now().UnixNano())
randomRatio := float64(rand.Intn(17)+33) * 0.01
certmagic.Default.RenewalWindowRatio = randomRatio
}
func (c *certmagicProvider) Listen(hosts ...string) (net.Listener, error) {
c.setup()
return certmagic.Listen(hosts)
}
func (c *certmagicProvider) TLSConfig(hosts ...string) (*tls.Config, error) {
c.setup()
return certmagic.TLS(hosts)
}
// NewProvider returns a certmagic provider
func NewProvider(options ...acme.Option) acme.Provider {
opts := acme.DefaultOptions()
for _, o := range options {
o(&opts)
}
if opts.Cache != nil {
if _, ok := opts.Cache.(certmagic.Storage); !ok {
logger.Fatal("ACME: cache provided doesn't implement certmagic's Storage interface")
}
}
return &certmagicProvider{
opts: opts,
}
}

View File

@@ -1,147 +0,0 @@
package certmagic
import (
"bytes"
"encoding/gob"
"errors"
"fmt"
"path"
"strings"
"time"
"github.com/caddyserver/certmagic"
"github.com/micro/go-micro/v3/store"
"github.com/micro/go-micro/v3/sync"
)
// File represents a "File" that will be stored in store.Store - the contents and last modified time
type File struct {
// last modified time
LastModified time.Time
// Contents
Contents []byte
}
// storage is an implementation of certmagic.Storage using micro's sync.Map and store.Store interfaces.
// As certmagic storage expects a filesystem (with stat() abilities) we have to implement
// the bare minimum of metadata.
type storage struct {
lock sync.Sync
store store.Store
}
func (s *storage) Lock(key string) error {
return s.lock.Lock(key, sync.LockTTL(10*time.Minute))
}
func (s *storage) Unlock(key string) error {
return s.lock.Unlock(key)
}
func (s *storage) Store(key string, value []byte) error {
f := File{
LastModified: time.Now(),
Contents: value,
}
buf := &bytes.Buffer{}
e := gob.NewEncoder(buf)
if err := e.Encode(f); err != nil {
return err
}
r := &store.Record{
Key: key,
Value: buf.Bytes(),
}
return s.store.Write(r)
}
func (s *storage) Load(key string) ([]byte, error) {
if !s.Exists(key) {
return nil, certmagic.ErrNotExist(errors.New(key + " doesn't exist"))
}
records, err := s.store.Read(key)
if err != nil {
return nil, err
}
if len(records) != 1 {
return nil, fmt.Errorf("ACME Storage: multiple records matched key %s", key)
}
b := bytes.NewBuffer(records[0].Value)
d := gob.NewDecoder(b)
var f File
err = d.Decode(&f)
if err != nil {
return nil, err
}
return f.Contents, nil
}
func (s *storage) Delete(key string) error {
return s.store.Delete(key)
}
func (s *storage) Exists(key string) bool {
if _, err := s.store.Read(key); err != nil {
return false
}
return true
}
func (s *storage) List(prefix string, recursive bool) ([]string, error) {
keys, err := s.store.List()
if err != nil {
return nil, err
}
//nolint:prealloc
var results []string
for _, k := range keys {
if strings.HasPrefix(k, prefix) {
results = append(results, k)
}
}
if recursive {
return results, nil
}
keysMap := make(map[string]bool)
for _, key := range results {
dir := strings.Split(strings.TrimPrefix(key, prefix+"/"), "/")
keysMap[dir[0]] = true
}
results = make([]string, 0)
for k := range keysMap {
results = append(results, path.Join(prefix, k))
}
return results, nil
}
func (s *storage) Stat(key string) (certmagic.KeyInfo, error) {
records, err := s.store.Read(key)
if err != nil {
return certmagic.KeyInfo{}, err
}
if len(records) != 1 {
return certmagic.KeyInfo{}, fmt.Errorf("ACME Storage: multiple records matched key %s", key)
}
b := bytes.NewBuffer(records[0].Value)
d := gob.NewDecoder(b)
var f File
err = d.Decode(&f)
if err != nil {
return certmagic.KeyInfo{}, err
}
return certmagic.KeyInfo{
Key: key,
Modified: f.LastModified,
Size: int64(len(f.Contents)),
IsTerminal: false,
}, nil
}
// NewStorage returns a certmagic.Storage backed by a go-micro/lock and go-micro/store
func NewStorage(lock sync.Sync, store store.Store) certmagic.Storage {
return &storage{
lock: lock,
store: store,
}
}

View File

@@ -1,73 +0,0 @@
package acme
import "github.com/go-acme/lego/v3/challenge"
// Option (or Options) are passed to New() to configure providers
type Option func(o *Options)
// Options represents various options you can present to ACME providers
type Options struct {
// AcceptTLS must be set to true to indicate that you have read your
// provider's terms of service.
AcceptToS bool
// CA is the CA to use
CA string
// ChallengeProvider is a go-acme/lego challenge provider. Set this if you
// want to use DNS Challenges. Otherwise, tls-alpn-01 will be used
ChallengeProvider challenge.Provider
// Issue certificates for domains on demand. Otherwise, certs will be
// retrieved / issued on start-up.
OnDemand bool
// Cache is a storage interface. Most ACME libraries have an cache, but
// there's no defined interface, so if you consume this option
// sanity check it before using.
Cache interface{}
}
// AcceptToS indicates whether you accept your CA's terms of service
func AcceptToS(b bool) Option {
return func(o *Options) {
o.AcceptToS = b
}
}
// CA sets the CA of an acme.Options
func CA(CA string) Option {
return func(o *Options) {
o.CA = CA
}
}
// ChallengeProvider sets the Challenge provider of an acme.Options
// if set, it enables the DNS challenge, otherwise tls-alpn-01 will be used.
func ChallengeProvider(p challenge.Provider) Option {
return func(o *Options) {
o.ChallengeProvider = p
}
}
// OnDemand enables on-demand certificate issuance. Not recommended for use
// with the DNS challenge, as the first connection may be very slow.
func OnDemand(b bool) Option {
return func(o *Options) {
o.OnDemand = b
}
}
// Cache provides a cache / storage interface to the underlying ACME library
// as there is no standard, this needs to be validated by the underlying
// implentation.
func Cache(c interface{}) Option {
return func(o *Options) {
o.Cache = c
}
}
// DefaultOptions uses the Let's Encrypt Production CA, with DNS Challenge disabled.
func DefaultOptions() Options {
return Options{
AcceptToS: true,
CA: LetsEncryptProductionCA,
OnDemand: true,
}
}

View File

@@ -1,44 +0,0 @@
package cors
import (
"net/http"
)
// CombinedCORSHandler wraps a server and provides CORS headers
func CombinedCORSHandler(h http.Handler) http.Handler {
return corsHandler{h}
}
type corsHandler struct {
handler http.Handler
}
func (c corsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
SetHeaders(w, r)
if r.Method == "OPTIONS" {
return
}
c.handler.ServeHTTP(w, r)
}
// SetHeaders sets the CORS headers
func SetHeaders(w http.ResponseWriter, r *http.Request) {
set := func(w http.ResponseWriter, k, v string) {
if v := w.Header().Get(k); len(v) > 0 {
return
}
w.Header().Set(k, v)
}
if origin := r.Header.Get("Origin"); len(origin) > 0 {
set(w, "Access-Control-Allow-Origin", origin)
} else {
set(w, "Access-Control-Allow-Origin", "*")
}
set(w, "Access-Control-Allow-Credentials", "true")
set(w, "Access-Control-Allow-Methods", "POST, PATCH, GET, OPTIONS, PUT, DELETE")
set(w, "Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
}

View File

@@ -1,120 +0,0 @@
// Package http provides a http server with features; acme, cors, etc
package http
import (
"crypto/tls"
"net"
"net/http"
"os"
"sync"
"github.com/gorilla/handlers"
"github.com/micro/go-micro/v3/api/server"
"github.com/micro/go-micro/v3/api/server/cors"
"github.com/micro/go-micro/v3/logger"
)
type httpServer struct {
mux *http.ServeMux
opts server.Options
mtx sync.RWMutex
address string
exit chan chan error
}
func NewServer(address string, opts ...server.Option) server.Server {
var options server.Options
for _, o := range opts {
o(&options)
}
return &httpServer{
opts: options,
mux: http.NewServeMux(),
address: address,
exit: make(chan chan error),
}
}
func (s *httpServer) Address() string {
s.mtx.RLock()
defer s.mtx.RUnlock()
return s.address
}
func (s *httpServer) Init(opts ...server.Option) error {
for _, o := range opts {
o(&s.opts)
}
return nil
}
func (s *httpServer) Handle(path string, handler http.Handler) {
// TODO: move this stuff out to one place with ServeHTTP
// apply the wrappers, e.g. auth
for _, wrapper := range s.opts.Wrappers {
handler = wrapper(handler)
}
// wrap with cors
if s.opts.EnableCORS {
handler = cors.CombinedCORSHandler(handler)
}
// wrap with logger
handler = handlers.CombinedLoggingHandler(os.Stdout, handler)
s.mux.Handle(path, handler)
}
func (s *httpServer) Start() error {
var l net.Listener
var err error
if s.opts.EnableACME && s.opts.ACMEProvider != nil {
// should we check the address to make sure its using :443?
l, err = s.opts.ACMEProvider.Listen(s.opts.ACMEHosts...)
} else if s.opts.EnableTLS && s.opts.TLSConfig != nil {
l, err = tls.Listen("tcp", s.address, s.opts.TLSConfig)
} else {
// otherwise plain listen
l, err = net.Listen("tcp", s.address)
}
if err != nil {
return err
}
if logger.V(logger.InfoLevel, logger.DefaultLogger) {
logger.Infof("HTTP API Listening on %s", l.Addr().String())
}
s.mtx.Lock()
s.address = l.Addr().String()
s.mtx.Unlock()
go func() {
if err := http.Serve(l, s.mux); err != nil {
// temporary fix
//logger.Fatal(err)
}
}()
go func() {
ch := <-s.exit
ch <- l.Close()
}()
return nil
}
func (s *httpServer) Stop() error {
ch := make(chan error)
s.exit <- ch
return <-ch
}
func (s *httpServer) String() string {
return "http"
}

View File

@@ -1,41 +0,0 @@
package http
import (
"fmt"
"io/ioutil"
"net/http"
"testing"
)
func TestHTTPServer(t *testing.T) {
testResponse := "hello world"
s := NewServer("localhost:0")
s.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, testResponse)
}))
if err := s.Start(); err != nil {
t.Fatal(err)
}
rsp, err := http.Get(fmt.Sprintf("http://%s/", s.Address()))
if err != nil {
t.Fatal(err)
}
defer rsp.Body.Close()
b, err := ioutil.ReadAll(rsp.Body)
if err != nil {
t.Fatal(err)
}
if string(b) != testResponse {
t.Fatalf("Unexpected response, got %s, expected %s", string(b), testResponse)
}
if err := s.Stop(); err != nil {
t.Fatal(err)
}
}

View File

@@ -1,72 +0,0 @@
package server
import (
"crypto/tls"
"net/http"
"github.com/micro/go-micro/v3/api/resolver"
"github.com/micro/go-micro/v3/api/server/acme"
)
type Option func(o *Options)
type Options struct {
EnableACME bool
EnableCORS bool
ACMEProvider acme.Provider
EnableTLS bool
ACMEHosts []string
TLSConfig *tls.Config
Resolver resolver.Resolver
Wrappers []Wrapper
}
type Wrapper func(h http.Handler) http.Handler
func WrapHandler(w ...Wrapper) Option {
return func(o *Options) {
o.Wrappers = append(o.Wrappers, w...)
}
}
func EnableCORS(b bool) Option {
return func(o *Options) {
o.EnableCORS = b
}
}
func EnableACME(b bool) Option {
return func(o *Options) {
o.EnableACME = b
}
}
func ACMEHosts(hosts ...string) Option {
return func(o *Options) {
o.ACMEHosts = hosts
}
}
func ACMEProvider(p acme.Provider) Option {
return func(o *Options) {
o.ACMEProvider = p
}
}
func EnableTLS(b bool) Option {
return func(o *Options) {
o.EnableTLS = b
}
}
func TLSConfig(t *tls.Config) Option {
return func(o *Options) {
o.TLSConfig = t
}
}
func Resolver(r resolver.Resolver) Option {
return func(o *Options) {
o.Resolver = r
}
}

View File

@@ -1,15 +0,0 @@
// Package server provides an API gateway server which handles inbound requests
package server
import (
"net/http"
)
// Server serves api requests
type Server interface {
Address() string
Init(opts ...Option) error
Handle(path string, handler http.Handler)
Start() error
Stop() error
}

View File

@@ -5,6 +5,8 @@ import (
"context"
"errors"
"time"
"github.com/unistack-org/micro/v3/metadata"
)
const (
@@ -17,6 +19,8 @@ const (
)
var (
// DefaultAuth holds default auth implementation
DefaultAuth Auth = NewAuth()
// ErrInvalidToken is when the token provided is not valid
ErrInvalidToken = errors.New("invalid token provided")
// ErrForbidden is when a user does not have the necessary scope to access a resource
@@ -49,30 +53,30 @@ type Auth interface {
// Account provided by an auth provider
type Account struct {
// ID of the account e.g. email
// Metadata any other associated metadata
Metadata metadata.Metadata `json:"metadata"`
// ID of the account e.g. email or uuid
ID string `json:"id"`
// Type of the account, e.g. service
Type string `json:"type"`
// Issuer of the account
Issuer string `json:"issuer"`
// Any other associated metadata
Metadata map[string]string `json:"metadata"`
// Scopes the account has access to
Scopes []string `json:"scopes"`
// Secret for the account, e.g. the password
Secret string `json:"secret"`
// Scopes the account has access to
Scopes []string `json:"scopes"`
}
// Token can be short or long lived
type Token struct {
// The token to be used for accessing resources
AccessToken string `json:"access_token"`
// RefreshToken to be used to generate a new token
RefreshToken string `json:"refresh_token"`
// Time of token creation
Created time.Time `json:"created"`
// Time of token expiry
Expiry time.Time `json:"expiry"`
// The token to be used for accessing resources
AccessToken string `json:"access_token"`
// RefreshToken to be used to generate a new token
RefreshToken string `json:"refresh_token"`
}
// Expired returns a boolean indicating if the token needs to be refreshed
@@ -102,17 +106,15 @@ const (
// Rule is used to verify access to a resource
type Rule struct {
// ID of the rule, e.g. "public"
ID string
// Scope the rule requires, a blank scope indicates open to the public and * indicates the rule
// applies to any valid account
Scope string
// Resource the rule applies to
// Resource that rule belongs to
Resource *Resource
// Access determines if the rule grants or denies access to the resource
// ID of the rule
ID string
// Scope of the rule
Scope string
// Access flag allow/deny
Access Access
// Priority the rule should take when verifying a request, the higher the value the sooner the
// rule will be applied
// Priority holds the rule priority
Priority int32
}
@@ -123,11 +125,17 @@ type accountKey struct{}
// is not set, a nil account will be returned. The error is only returned
// when there was a problem retrieving an account
func AccountFromContext(ctx context.Context) (*Account, bool) {
if ctx == nil {
return nil, false
}
acc, ok := ctx.Value(accountKey{}).(*Account)
return acc, ok
}
// ContextWithAccount sets the account in the context
func ContextWithAccount(ctx context.Context, account *Account) context.Context {
if ctx == nil {
ctx = context.Background()
}
return context.WithValue(ctx, accountKey{}, account)
}

View File

@@ -1,151 +0,0 @@
// Package jwt is a jwt implementation of the auth interface
package jwt
import (
"sync"
"time"
"github.com/micro/go-micro/v3/auth"
"github.com/micro/go-micro/v3/util/token"
"github.com/micro/go-micro/v3/util/token/jwt"
)
// NewAuth returns a new instance of the Auth service
func NewAuth(opts ...auth.Option) auth.Auth {
j := new(jwtAuth)
j.Init(opts...)
return j
}
type jwtAuth struct {
options auth.Options
token token.Provider
rules []*auth.Rule
sync.Mutex
}
func (j *jwtAuth) String() string {
return "jwt"
}
func (j *jwtAuth) Init(opts ...auth.Option) {
j.Lock()
defer j.Unlock()
for _, o := range opts {
o(&j.options)
}
j.token = jwt.NewTokenProvider(
token.WithPrivateKey(j.options.PrivateKey),
token.WithPublicKey(j.options.PublicKey),
)
}
func (j *jwtAuth) Options() auth.Options {
j.Lock()
defer j.Unlock()
return j.options
}
func (j *jwtAuth) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) {
options := auth.NewGenerateOptions(opts...)
if len(options.Issuer) == 0 {
options.Issuer = j.Options().Issuer
}
account := &auth.Account{
ID: id,
Type: options.Type,
Scopes: options.Scopes,
Metadata: options.Metadata,
Issuer: options.Issuer,
}
// generate a JWT secret which can be provided to the Token() method
// and exchanged for an access token
secret, err := j.token.Generate(account, token.WithExpiry(time.Hour*24*365))
if err != nil {
return nil, err
}
account.Secret = secret.Token
// return the account
return account, nil
}
func (j *jwtAuth) Grant(rule *auth.Rule) error {
j.Lock()
defer j.Unlock()
j.rules = append(j.rules, rule)
return nil
}
func (j *jwtAuth) Revoke(rule *auth.Rule) error {
j.Lock()
defer j.Unlock()
rules := []*auth.Rule{}
for _, r := range j.rules {
if r.ID != rule.ID {
rules = append(rules, r)
}
}
j.rules = rules
return nil
}
func (j *jwtAuth) Verify(acc *auth.Account, res *auth.Resource, opts ...auth.VerifyOption) error {
j.Lock()
defer j.Unlock()
var options auth.VerifyOptions
for _, o := range opts {
o(&options)
}
return auth.VerifyAccess(j.rules, acc, res)
}
func (j *jwtAuth) Rules(opts ...auth.RulesOption) ([]*auth.Rule, error) {
j.Lock()
defer j.Unlock()
return j.rules, nil
}
func (j *jwtAuth) Inspect(token string) (*auth.Account, error) {
return j.token.Inspect(token)
}
func (j *jwtAuth) Token(opts ...auth.TokenOption) (*auth.Token, error) {
options := auth.NewTokenOptions(opts...)
secret := options.RefreshToken
if len(options.Secret) > 0 {
secret = options.Secret
}
account, err := j.token.Inspect(secret)
if err != nil {
return nil, err
}
access, err := j.token.Generate(account, token.WithExpiry(options.Expiry))
if err != nil {
return nil, err
}
refresh, err := j.token.Generate(account, token.WithExpiry(options.Expiry+time.Hour))
if err != nil {
return nil, err
}
return &auth.Token{
Created: access.Created,
Expiry: access.Expiry,
AccessToken: access.Token,
RefreshToken: refresh.Token,
}, nil
}

78
auth/noop.go Normal file
View File

@@ -0,0 +1,78 @@
package auth
import (
"github.com/google/uuid"
)
type noopAuth struct {
opts Options
}
// String returns the name of the implementation
func (n *noopAuth) String() string {
return "noop"
}
// Init the auth
func (n *noopAuth) Init(opts ...Option) {
for _, o := range opts {
o(&n.opts)
}
}
// Options set for auth
func (n *noopAuth) Options() Options {
return n.opts
}
// Generate a new account
func (n *noopAuth) Generate(id string, opts ...GenerateOption) (*Account, error) {
options := NewGenerateOptions(opts...)
return &Account{
ID: id,
Secret: options.Secret,
Metadata: options.Metadata,
Scopes: options.Scopes,
Issuer: n.Options().Issuer,
}, nil
}
// Grant access to a resource
func (n *noopAuth) Grant(rule *Rule) error {
return nil
}
// Revoke access to a resource
func (n *noopAuth) Revoke(rule *Rule) error {
return nil
}
// Rules used to verify requests
func (n *noopAuth) Rules(opts ...RulesOption) ([]*Rule, error) {
return []*Rule{}, nil
}
// Verify an account has access to a resource
func (n *noopAuth) Verify(acc *Account, res *Resource, opts ...VerifyOption) error {
return nil
}
// Inspect a token
func (n *noopAuth) Inspect(token string) (*Account, error) {
uid, err := uuid.NewRandom()
if err != nil {
return nil, err
}
return &Account{ID: uid.String(), Issuer: n.Options().Issuer}, nil
}
// Token generation using an account id and secret
func (n *noopAuth) Token(opts ...TokenOption) (*Token, error) {
return &Token{}, nil
}
// NewAuth returns new noop auth
func NewAuth(opts ...Option) Auth {
return &noopAuth{opts: NewOptions(opts...)}
}

View File

@@ -1,81 +0,0 @@
package noop
import (
"github.com/google/uuid"
"github.com/micro/go-micro/v3/auth"
)
func NewAuth(opts ...auth.Option) auth.Auth {
var options auth.Options
for _, o := range opts {
o(&options)
}
return &noop{
opts: options,
}
}
type noop struct {
opts auth.Options
}
// String returns the name of the implementation
func (n *noop) String() string {
return "noop"
}
// Init the auth
func (n *noop) Init(opts ...auth.Option) {
for _, o := range opts {
o(&n.opts)
}
}
// Options set for auth
func (n *noop) Options() auth.Options {
return n.opts
}
// Generate a new account
func (n *noop) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) {
options := auth.NewGenerateOptions(opts...)
return &auth.Account{
ID: id,
Secret: options.Secret,
Metadata: options.Metadata,
Scopes: options.Scopes,
Issuer: n.Options().Issuer,
}, nil
}
// Grant access to a resource
func (n *noop) Grant(rule *auth.Rule) error {
return nil
}
// Revoke access to a resource
func (n *noop) Revoke(rule *auth.Rule) error {
return nil
}
// Rules used to verify requests
func (n *noop) Rules(opts ...auth.RulesOption) ([]*auth.Rule, error) {
return []*auth.Rule{}, nil
}
// Verify an account has access to a resource
func (n *noop) Verify(acc *auth.Account, res *auth.Resource, opts ...auth.VerifyOption) error {
return nil
}
// Inspect a token
func (n *noop) Inspect(token string) (*auth.Account, error) {
return &auth.Account{ID: uuid.New().String(), Issuer: n.Options().Issuer}, nil
}
// Token generation using an account id and secret
func (n *noop) Token(opts ...auth.TokenOption) (*auth.Token, error) {
return &auth.Token{}, nil
}

View File

@@ -4,40 +4,59 @@ import (
"context"
"time"
"github.com/micro/go-micro/v3/store"
"github.com/unistack-org/micro/v3/logger"
"github.com/unistack-org/micro/v3/metadata"
"github.com/unistack-org/micro/v3/meter"
"github.com/unistack-org/micro/v3/store"
"github.com/unistack-org/micro/v3/tracer"
)
// NewOptions creates Options struct from slice of options
func NewOptions(opts ...Option) Options {
var options Options
options := Options{
Tracer: tracer.DefaultTracer,
Logger: logger.DefaultLogger,
Meter: meter.DefaultMeter,
}
for _, o := range opts {
o(&options)
}
return options
}
// Options struct holds auth options
type Options struct {
// Issuer of the service's account
Issuer string
// ID is the services auth ID
ID string
// Secret is used to authenticate the service
Secret string
// Context holds the external options
Context context.Context
// Meter used for metrics
Meter meter.Meter
// Logger used for logging
Logger logger.Logger
// Tracer used for tracing
Tracer tracer.Tracer
// Store used for stre data
Store store.Store
// Token is the services token used to authenticate itself
Token *Token
// PublicKey for decoding JWTs
PublicKey string
// PrivateKey for encoding JWTs
PrivateKey string
// LoginURL is the relative url path where a user can login
LoginURL string
// Store to back auth
Store store.Store
// PrivateKey for encoding JWTs
PrivateKey string
// PublicKey for decoding JWTs
PublicKey string
// Secret is used to authenticate the service
Secret string
// ID is the services auth ID
ID string
// Issuer of the service's account
Issuer string
// Name holds the auth name
Name string
// Addrs sets the addresses of auth
Addrs []string
// Context to store other options
Context context.Context
}
// Option func
type Option func(o *Options)
// Addrs is the auth addresses to use
@@ -47,6 +66,13 @@ func Addrs(addrs ...string) Option {
}
}
// Name sets the name
func Name(n string) Option {
return func(o *Options) {
o.Name = n
}
}
// Issuer of the services account
func Issuer(i string) Option {
return func(o *Options) {
@@ -97,21 +123,17 @@ func LoginURL(url string) Option {
}
}
// GenerateOptions struct
type GenerateOptions struct {
// Metadata associated with the account
Metadata map[string]string
// Scopes the account has access too
Scopes []string
// Provider of the account, e.g. oauth
Metadata metadata.Metadata
Provider string
// Type of the account, e.g. user
Type string
// Secret used to authenticate the account
Secret string
// Issuer of the account, e.g. micro
Issuer string
Type string
Secret string
Issuer string
Scopes []string
}
// GenerateOption func
type GenerateOption func(o *GenerateOptions)
// WithSecret for the generated account
@@ -129,9 +151,9 @@ func WithType(t string) GenerateOption {
}
// WithMetadata for the generated account
func WithMetadata(md map[string]string) GenerateOption {
func WithMetadata(md metadata.Metadata) GenerateOption {
return func(o *GenerateOptions) {
o.Metadata = md
o.Metadata = metadata.Copy(md)
}
}
@@ -165,19 +187,16 @@ func NewGenerateOptions(opts ...GenerateOption) GenerateOptions {
return options
}
// TokenOptions struct
type TokenOptions struct {
// ID for the account
ID string
// Secret for the account
Secret string
// RefreshToken is used to refesh a token
ID string
Secret string
RefreshToken string
// Expiry is the time the token should live for
Expiry time.Duration
// Issuer of the account
Issuer string
Issuer string
Expiry time.Duration
}
// TokenOption func
type TokenOption func(o *TokenOptions)
// WithExpiry for the token
@@ -187,6 +206,7 @@ func WithExpiry(ex time.Duration) TokenOption {
}
}
// WithCredentials sets tye id and secret
func WithCredentials(id, secret string) TokenOption {
return func(o *TokenOptions) {
o.ID = id
@@ -194,12 +214,14 @@ func WithCredentials(id, secret string) TokenOption {
}
}
// WithToken sets the refresh token
func WithToken(rt string) TokenOption {
return func(o *TokenOptions) {
o.RefreshToken = rt
}
}
// WithTokenIssuer sets the token issuer option
func WithTokenIssuer(iss string) TokenOption {
return func(o *TokenOptions) {
o.Issuer = iss
@@ -213,7 +235,7 @@ func NewTokenOptions(opts ...TokenOption) TokenOptions {
o(&options)
}
// set defualt expiry of token
// set default expiry of token
if options.Expiry == 0 {
options.Expiry = time.Minute
}
@@ -221,39 +243,69 @@ func NewTokenOptions(opts ...TokenOption) TokenOptions {
return options
}
// VerifyOptions struct
type VerifyOptions struct {
Context context.Context
Namespace string
}
// VerifyOption func
type VerifyOption func(o *VerifyOptions)
// VerifyContext pass context to verify
func VerifyContext(ctx context.Context) VerifyOption {
return func(o *VerifyOptions) {
o.Context = ctx
}
}
// VerifyNamespace sets thhe namespace for verify
func VerifyNamespace(ns string) VerifyOption {
return func(o *VerifyOptions) {
o.Namespace = ns
}
}
// RulesOptions struct
type RulesOptions struct {
Context context.Context
Namespace string
}
// RulesOption func
type RulesOption func(o *RulesOptions)
// RulesContext pass rules context
func RulesContext(ctx context.Context) RulesOption {
return func(o *RulesOptions) {
o.Context = ctx
}
}
// RulesNamespace sets the rule namespace
func RulesNamespace(ns string) RulesOption {
return func(o *RulesOptions) {
o.Namespace = ns
}
}
// Logger sets the logger
func Logger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// Meter sets the meter
func Meter(m meter.Meter) Option {
return func(o *Options) {
o.Meter = m
}
}
// Tracer sets the meter
func Tracer(t tracer.Tracer) Option {
return func(o *Options) {
o.Tracer = t
}
}

View File

@@ -9,6 +9,7 @@ import (
// VerifyAccess an account has access to a resource using the rules provided. If the account does not have
// access an error will be returned. If there are no rules provided which match the resource, an error
// will be returned
//nolint:gocyclo
func VerifyAccess(rules []*Rule, acc *Account, res *Resource) error {
// the rule is only to be applied if the type matches the resource or is catch-all (*)
validTypes := []string{"*", res.Type}
@@ -83,7 +84,7 @@ func VerifyAccess(rules []*Rule, acc *Account, res *Resource) error {
// not case sensitive.
func include(slice []string, val string) bool {
for _, s := range slice {
if strings.ToLower(s) == strings.ToLower(val) {
if strings.EqualFold(s, val) {
return true
}
}

View File

@@ -42,7 +42,7 @@ func TestVerify(t *testing.T) {
Account: &Account{},
Resource: srvResource,
Rules: []*Rule{
&Rule{
{
Scope: "",
Resource: catchallResource,
},
@@ -52,7 +52,7 @@ func TestVerify(t *testing.T) {
Name: "CatchallPublicNoAccount",
Resource: srvResource,
Rules: []*Rule{
&Rule{
{
Scope: "",
Resource: catchallResource,
},
@@ -63,7 +63,7 @@ func TestVerify(t *testing.T) {
Account: &Account{},
Resource: srvResource,
Rules: []*Rule{
&Rule{
{
Scope: "*",
Resource: catchallResource,
},
@@ -73,7 +73,7 @@ func TestVerify(t *testing.T) {
Name: "CatchallPrivateNoAccount",
Resource: srvResource,
Rules: []*Rule{
&Rule{
{
Scope: "*",
Resource: catchallResource,
},
@@ -85,7 +85,7 @@ func TestVerify(t *testing.T) {
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
&Rule{
{
Scope: "*",
Resource: &Resource{
Type: srvResource.Type,
@@ -100,7 +100,7 @@ func TestVerify(t *testing.T) {
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
&Rule{
{
Scope: "*",
Resource: &Resource{
Type: srvResource.Type,
@@ -118,7 +118,7 @@ func TestVerify(t *testing.T) {
Scopes: []string{"neededscope"},
},
Rules: []*Rule{
&Rule{
{
Scope: "neededscope",
Resource: srvResource,
},
@@ -131,7 +131,7 @@ func TestVerify(t *testing.T) {
Scopes: []string{"neededscope"},
},
Rules: []*Rule{
&Rule{
{
Scope: "invalidscope",
Resource: srvResource,
},
@@ -143,7 +143,7 @@ func TestVerify(t *testing.T) {
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
&Rule{
{
Scope: "*",
Resource: catchallResource,
Access: AccessDenied,
@@ -156,7 +156,7 @@ func TestVerify(t *testing.T) {
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
&Rule{
{
Scope: "*",
Resource: catchallResource,
Access: AccessDenied,
@@ -169,13 +169,13 @@ func TestVerify(t *testing.T) {
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
&Rule{
{
Scope: "*",
Resource: catchallResource,
Access: AccessGranted,
Priority: 1,
},
&Rule{
{
Scope: "*",
Resource: catchallResource,
Access: AccessDenied,
@@ -188,13 +188,13 @@ func TestVerify(t *testing.T) {
Resource: srvResource,
Account: &Account{},
Rules: []*Rule{
&Rule{
{
Scope: "*",
Resource: catchallResource,
Access: AccessGranted,
Priority: 0,
},
&Rule{
{
Scope: "*",
Resource: catchallResource,
Access: AccessDenied,
@@ -208,7 +208,7 @@ func TestVerify(t *testing.T) {
Resource: webResource,
Account: &Account{},
Rules: []*Rule{
&Rule{
{
Scope: "*",
Resource: webResource,
},
@@ -219,7 +219,7 @@ func TestVerify(t *testing.T) {
Resource: webResource,
Account: &Account{},
Rules: []*Rule{
&Rule{
{
Scope: "*",
Resource: &Resource{
Type: webResource.Type,
@@ -235,7 +235,7 @@ func TestVerify(t *testing.T) {
Resource: webResource,
Account: &Account{},
Rules: []*Rule{
&Rule{
{
Scope: "*",
Resource: &Resource{
Type: webResource.Type,
@@ -250,7 +250,7 @@ func TestVerify(t *testing.T) {
Resource: webResource,
Account: &Account{},
Rules: []*Rule{
&Rule{
{
Scope: "*",
Resource: &Resource{
Type: webResource.Type,
@@ -265,7 +265,7 @@ func TestVerify(t *testing.T) {
Resource: webResource,
Account: &Account{},
Rules: []*Rule{
&Rule{
{
Scope: "*",
Resource: &Resource{
Type: webResource.Type,

View File

@@ -1,31 +1,50 @@
// Package broker is an interface used for asynchronous messaging
package broker
import (
"context"
"github.com/unistack-org/micro/v3/metadata"
)
var (
// DefaultBroker default broker
DefaultBroker Broker = NewBroker()
)
// Broker is an interface used for asynchronous messaging.
type Broker interface {
Name() string
Init(...Option) error
Options() Options
Address() string
Connect() error
Disconnect() error
Publish(topic string, m *Message, opts ...PublishOption) error
Subscribe(topic string, h Handler, opts ...SubscribeOption) (Subscriber, error)
Connect(context.Context) error
Disconnect(context.Context) error
Publish(context.Context, string, *Message, ...PublishOption) error
Subscribe(context.Context, string, Handler, ...SubscribeOption) (Subscriber, error)
String() string
}
// Handler is used to process messages via a subscription of a topic.
type Handler func(*Message) error
type Handler func(Event) error
type ErrorHandler func(*Message, error)
// Event is given to a subscription handler for processing
type Event interface {
Topic() string
Message() *Message
Ack() error
Error() error
}
// Message is used to transfer data
type Message struct {
Header map[string]string
Body []byte
Header metadata.Metadata // contains message metadata
Body []byte // contains message body
}
// Subscriber is a convenience return type for the Subscribe method
type Subscriber interface {
Options() SubscribeOptions
Topic() string
Unsubscribe() error
Unsubscribe(context.Context) error
}

54
broker/context.go Normal file
View File

@@ -0,0 +1,54 @@
package broker
import (
"context"
)
type brokerKey struct{}
// FromContext returns broker from passed context
func FromContext(ctx context.Context) (Broker, bool) {
if ctx == nil {
return nil, false
}
c, ok := ctx.Value(brokerKey{}).(Broker)
return c, ok
}
// NewContext savess broker in context
func NewContext(ctx context.Context, s Broker) context.Context {
if ctx == nil {
ctx = context.Background()
}
return context.WithValue(ctx, brokerKey{}, s)
}
// SetSubscribeOption returns a function to setup a context with given value
func SetSubscribeOption(k, v interface{}) SubscribeOption {
return func(o *SubscribeOptions) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}
// SetOption returns a function to setup a context with given value
func SetOption(k, v interface{}) Option {
return func(o *Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}
// SetPublishOption returns a function to setup a context with given value
func SetPublishOption(k, v interface{}) PublishOption {
return func(o *PublishOptions) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}

View File

@@ -1,689 +0,0 @@
// Package http provides a http based message broker
package http
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net"
"net/http"
"net/url"
"runtime"
"sync"
"time"
"github.com/google/uuid"
"github.com/micro/go-micro/v3/broker"
"github.com/micro/go-micro/v3/codec/json"
merr "github.com/micro/go-micro/v3/errors"
"github.com/micro/go-micro/v3/registry"
"github.com/micro/go-micro/v3/registry/cache"
"github.com/micro/go-micro/v3/registry/mdns"
maddr "github.com/micro/go-micro/v3/util/addr"
mnet "github.com/micro/go-micro/v3/util/net"
mls "github.com/micro/go-micro/v3/util/tls"
"golang.org/x/net/http2"
)
// HTTP Broker is a point to point async broker
type httpBroker struct {
id string
address string
opts broker.Options
mux *http.ServeMux
c *http.Client
r registry.Registry
sync.RWMutex
subscribers map[string][]*httpSubscriber
running bool
exit chan chan error
// offline message inbox
mtx sync.RWMutex
inbox map[string][][]byte
}
type httpSubscriber struct {
opts broker.SubscribeOptions
id string
topic string
fn broker.Handler
svc *registry.Service
hb *httpBroker
}
var (
DefaultPath = "/"
DefaultAddress = "127.0.0.1:0"
serviceName = "micro.http.broker"
broadcastVersion = "ff.http.broadcast"
registerTTL = time.Minute
registerInterval = time.Second * 30
)
func init() {
rand.Seed(time.Now().Unix())
}
func newTransport(config *tls.Config) *http.Transport {
if config == nil {
config = &tls.Config{
InsecureSkipVerify: true,
}
}
dialTLS := func(network string, addr string) (net.Conn, error) {
return tls.Dial(network, addr, config)
}
t := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
DialTLS: dialTLS,
}
runtime.SetFinalizer(&t, func(tr **http.Transport) {
(*tr).CloseIdleConnections()
})
// setup http2
http2.ConfigureTransport(t)
return t
}
func newHttpBroker(opts ...broker.Option) broker.Broker {
options := broker.Options{
Codec: json.Marshaler{},
Context: context.TODO(),
Registry: mdns.NewRegistry(),
}
for _, o := range opts {
o(&options)
}
// set address
addr := DefaultAddress
if len(options.Addrs) > 0 && len(options.Addrs[0]) > 0 {
addr = options.Addrs[0]
}
h := &httpBroker{
id: uuid.New().String(),
address: addr,
opts: options,
r: options.Registry,
c: &http.Client{Transport: newTransport(options.TLSConfig)},
subscribers: make(map[string][]*httpSubscriber),
exit: make(chan chan error),
mux: http.NewServeMux(),
inbox: make(map[string][][]byte),
}
// specify the message handler
h.mux.Handle(DefaultPath, h)
// get optional handlers
if h.opts.Context != nil {
handlers, ok := h.opts.Context.Value("http_handlers").(map[string]http.Handler)
if ok {
for pattern, handler := range handlers {
h.mux.Handle(pattern, handler)
}
}
}
return h
}
func (h *httpSubscriber) Options() broker.SubscribeOptions {
return h.opts
}
func (h *httpSubscriber) Topic() string {
return h.topic
}
func (h *httpSubscriber) Unsubscribe() error {
return h.hb.unsubscribe(h)
}
func (h *httpBroker) saveMessage(topic string, msg []byte) {
h.mtx.Lock()
defer h.mtx.Unlock()
// get messages
c := h.inbox[topic]
// save message
c = append(c, msg)
// max length 64
if len(c) > 64 {
c = c[:64]
}
// save inbox
h.inbox[topic] = c
}
func (h *httpBroker) getMessage(topic string, num int) [][]byte {
h.mtx.Lock()
defer h.mtx.Unlock()
// get messages
c, ok := h.inbox[topic]
if !ok {
return nil
}
// more message than requests
if len(c) >= num {
msg := c[:num]
h.inbox[topic] = c[num:]
return msg
}
// reset inbox
h.inbox[topic] = nil
// return all messages
return c
}
func (h *httpBroker) subscribe(s *httpSubscriber) error {
h.Lock()
defer h.Unlock()
if err := h.r.Register(s.svc, registry.RegisterTTL(registerTTL)); err != nil {
return err
}
h.subscribers[s.topic] = append(h.subscribers[s.topic], s)
return nil
}
func (h *httpBroker) unsubscribe(s *httpSubscriber) error {
h.Lock()
defer h.Unlock()
//nolint:prealloc
var subscribers []*httpSubscriber
// look for subscriber
for _, sub := range h.subscribers[s.topic] {
// deregister and skip forward
if sub == s {
_ = h.r.Deregister(sub.svc)
continue
}
// keep subscriber
subscribers = append(subscribers, sub)
}
// set subscribers
h.subscribers[s.topic] = subscribers
return nil
}
func (h *httpBroker) run(l net.Listener) {
t := time.NewTicker(registerInterval)
defer t.Stop()
for {
select {
// heartbeat for each subscriber
case <-t.C:
h.RLock()
for _, subs := range h.subscribers {
for _, sub := range subs {
_ = h.r.Register(sub.svc, registry.RegisterTTL(registerTTL))
}
}
h.RUnlock()
// received exit signal
case ch := <-h.exit:
ch <- l.Close()
h.RLock()
for _, subs := range h.subscribers {
for _, sub := range subs {
_ = h.r.Deregister(sub.svc)
}
}
h.RUnlock()
return
}
}
}
func (h *httpBroker) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
err := merr.BadRequest("go.micro.broker", "Method not allowed")
http.Error(w, err.Error(), http.StatusMethodNotAllowed)
return
}
defer req.Body.Close()
req.ParseForm()
b, err := ioutil.ReadAll(req.Body)
if err != nil {
errr := merr.InternalServerError("go.micro.broker", "Error reading request body: %v", err)
w.WriteHeader(500)
w.Write([]byte(errr.Error()))
return
}
var msg *broker.Message
if err = h.opts.Codec.Unmarshal(b, &msg); err != nil {
errr := merr.InternalServerError("go.micro.broker", "Error parsing request body: %v", err)
w.WriteHeader(500)
w.Write([]byte(errr.Error()))
return
}
topic := msg.Header["Micro-Topic"]
if len(topic) == 0 {
errr := merr.InternalServerError("go.micro.broker", "Topic not found")
w.WriteHeader(500)
w.Write([]byte(errr.Error()))
return
}
id := req.Form.Get("id")
//nolint:prealloc
var subs []broker.Handler
h.RLock()
for _, subscriber := range h.subscribers[topic] {
if id != subscriber.id {
continue
}
subs = append(subs, subscriber.fn)
}
h.RUnlock()
// execute the handler
for _, fn := range subs {
fn(msg)
}
}
func (h *httpBroker) Address() string {
h.RLock()
defer h.RUnlock()
return h.address
}
func (h *httpBroker) Connect() error {
h.RLock()
if h.running {
h.RUnlock()
return nil
}
h.RUnlock()
h.Lock()
defer h.Unlock()
var l net.Listener
var err error
if h.opts.Secure || h.opts.TLSConfig != nil {
config := h.opts.TLSConfig
fn := func(addr string) (net.Listener, error) {
if config == nil {
hosts := []string{addr}
// check if its a valid host:port
if host, _, err := net.SplitHostPort(addr); err == nil {
if len(host) == 0 {
hosts = maddr.IPs()
} else {
hosts = []string{host}
}
}
// generate a certificate
cert, err := mls.Certificate(hosts...)
if err != nil {
return nil, err
}
config = &tls.Config{Certificates: []tls.Certificate{cert}}
}
return tls.Listen("tcp", addr, config)
}
l, err = mnet.Listen(h.address, fn)
} else {
fn := func(addr string) (net.Listener, error) {
return net.Listen("tcp", addr)
}
l, err = mnet.Listen(h.address, fn)
}
if err != nil {
return err
}
addr := h.address
h.address = l.Addr().String()
go http.Serve(l, h.mux)
go func() {
h.run(l)
h.Lock()
h.opts.Addrs = []string{addr}
h.address = addr
h.Unlock()
}()
// get registry
reg := h.opts.Registry
if reg == nil {
reg = mdns.NewRegistry()
}
// set cache
h.r = cache.New(reg)
// set running
h.running = true
return nil
}
func (h *httpBroker) Disconnect() error {
h.RLock()
if !h.running {
h.RUnlock()
return nil
}
h.RUnlock()
h.Lock()
defer h.Unlock()
// stop cache
rc, ok := h.r.(cache.Cache)
if ok {
rc.Stop()
}
// exit and return err
ch := make(chan error)
h.exit <- ch
err := <-ch
// set not running
h.running = false
return err
}
func (h *httpBroker) Init(opts ...broker.Option) error {
h.RLock()
if h.running {
h.RUnlock()
return errors.New("cannot init while connected")
}
h.RUnlock()
h.Lock()
defer h.Unlock()
for _, o := range opts {
o(&h.opts)
}
if len(h.opts.Addrs) > 0 && len(h.opts.Addrs[0]) > 0 {
h.address = h.opts.Addrs[0]
}
if len(h.id) == 0 {
h.id = "go.micro.http.broker-" + uuid.New().String()
}
// get registry
reg := h.opts.Registry
if reg == nil {
reg = mdns.NewRegistry()
}
// get cache
if rc, ok := h.r.(cache.Cache); ok {
rc.Stop()
}
// set registry
h.r = cache.New(reg)
// reconfigure tls config
if c := h.opts.TLSConfig; c != nil {
h.c = &http.Client{
Transport: newTransport(c),
}
}
return nil
}
func (h *httpBroker) Options() broker.Options {
return h.opts
}
func (h *httpBroker) Publish(topic string, msg *broker.Message, opts ...broker.PublishOption) error {
// create the message first
m := &broker.Message{
Header: make(map[string]string),
Body: msg.Body,
}
for k, v := range msg.Header {
m.Header[k] = v
}
m.Header["Micro-Topic"] = topic
// encode the message
b, err := h.opts.Codec.Marshal(m)
if err != nil {
return err
}
// save the message
h.saveMessage(topic, b)
// now attempt to get the service
h.RLock()
s, err := h.r.GetService(serviceName)
if err != nil {
h.RUnlock()
return err
}
h.RUnlock()
pub := func(node *registry.Node, t string, b []byte) error {
scheme := "http"
// check if secure is added in metadata
if node.Metadata["secure"] == "true" {
scheme = "https"
}
vals := url.Values{}
vals.Add("id", node.Id)
uri := fmt.Sprintf("%s://%s%s?%s", scheme, node.Address, DefaultPath, vals.Encode())
r, err := h.c.Post(uri, "application/json", bytes.NewReader(b))
if err != nil {
return err
}
// discard response body
io.Copy(ioutil.Discard, r.Body)
r.Body.Close()
return nil
}
srv := func(s []*registry.Service, b []byte) {
for _, service := range s {
var nodes []*registry.Node
for _, node := range service.Nodes {
// only use nodes tagged with broker http
if node.Metadata["broker"] != "http" {
continue
}
// look for nodes for the topic
if node.Metadata["topic"] != topic {
continue
}
nodes = append(nodes, node)
}
// only process if we have nodes
if len(nodes) == 0 {
continue
}
switch service.Version {
// broadcast version means broadcast to all nodes
case broadcastVersion:
var success bool
// publish to all nodes
for _, node := range nodes {
// publish async
if err := pub(node, topic, b); err == nil {
success = true
}
}
// save if it failed to publish at least once
if !success {
h.saveMessage(topic, b)
}
default:
// select node to publish to
node := nodes[rand.Int()%len(nodes)]
// publish async to one node
if err := pub(node, topic, b); err != nil {
// if failed save it
h.saveMessage(topic, b)
}
}
}
}
// do the rest async
go func() {
// get a third of the backlog
messages := h.getMessage(topic, 8)
delay := (len(messages) > 1)
// publish all the messages
for _, msg := range messages {
// serialize here
srv(s, msg)
// sending a backlog of messages
if delay {
time.Sleep(time.Millisecond * 100)
}
}
}()
return nil
}
func (h *httpBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
var err error
var host, port string
options := broker.NewSubscribeOptions(opts...)
// parse address for host, port
host, port, err = net.SplitHostPort(h.Address())
if err != nil {
return nil, err
}
addr, err := maddr.Extract(host)
if err != nil {
return nil, err
}
var secure bool
if h.opts.Secure || h.opts.TLSConfig != nil {
secure = true
}
// register service
node := &registry.Node{
Id: topic + "-" + h.id,
Address: mnet.HostPort(addr, port),
Metadata: map[string]string{
"secure": fmt.Sprintf("%t", secure),
"broker": "http",
"topic": topic,
},
}
// check for queue group or broadcast queue
version := options.Queue
if len(version) == 0 {
version = broadcastVersion
}
service := &registry.Service{
Name: serviceName,
Version: version,
Nodes: []*registry.Node{node},
}
// generate subscriber
subscriber := &httpSubscriber{
opts: options,
hb: h,
id: node.Id,
topic: topic,
fn: handler,
svc: service,
}
// subscribe now
if err := h.subscribe(subscriber); err != nil {
return nil, err
}
// return the subscriber
return subscriber, nil
}
func (h *httpBroker) String() string {
return "http"
}
// NewBroker returns a new http broker
func NewBroker(opts ...broker.Option) broker.Broker {
return newHttpBroker(opts...)
}

View File

@@ -1,377 +0,0 @@
package http
import (
"sync"
"testing"
"time"
"github.com/google/uuid"
"github.com/micro/go-micro/v3/broker"
"github.com/micro/go-micro/v3/registry"
"github.com/micro/go-micro/v3/registry/memory"
)
var (
// mock data
testData = map[string][]*registry.Service{
"foo": {
{
Name: "foo",
Version: "1.0.0",
Nodes: []*registry.Node{
{
Id: "foo-1.0.0-123",
Address: "localhost:9999",
},
{
Id: "foo-1.0.0-321",
Address: "localhost:9999",
},
},
},
{
Name: "foo",
Version: "1.0.1",
Nodes: []*registry.Node{
{
Id: "foo-1.0.1-321",
Address: "localhost:6666",
},
},
},
{
Name: "foo",
Version: "1.0.3",
Nodes: []*registry.Node{
{
Id: "foo-1.0.3-345",
Address: "localhost:8888",
},
},
},
},
}
)
func newTestRegistry() registry.Registry {
return memory.NewRegistry(memory.Services(testData))
}
func sub(be *testing.B, c int) {
be.StopTimer()
m := newTestRegistry()
b := NewBroker(broker.Registry(m))
topic := uuid.New().String()
if err := b.Init(); err != nil {
be.Fatalf("Unexpected init error: %v", err)
}
if err := b.Connect(); err != nil {
be.Fatalf("Unexpected connect error: %v", err)
}
msg := &broker.Message{
Header: map[string]string{
"Content-Type": "application/json",
},
Body: []byte(`{"message": "Hello World"}`),
}
var subs []broker.Subscriber
done := make(chan bool, c)
for i := 0; i < c; i++ {
sub, err := b.Subscribe(topic, func(m *broker.Message) error {
done <- true
if string(m.Body) != string(msg.Body) {
be.Fatalf("Unexpected msg %s, expected %s", string(m.Body), string(msg.Body))
}
return nil
}, broker.Queue("shared"))
if err != nil {
be.Fatalf("Unexpected subscribe error: %v", err)
}
subs = append(subs, sub)
}
for i := 0; i < be.N; i++ {
be.StartTimer()
if err := b.Publish(topic, msg); err != nil {
be.Fatalf("Unexpected publish error: %v", err)
}
<-done
be.StopTimer()
}
for _, sub := range subs {
sub.Unsubscribe()
}
if err := b.Disconnect(); err != nil {
be.Fatalf("Unexpected disconnect error: %v", err)
}
}
func pub(be *testing.B, c int) {
be.StopTimer()
m := newTestRegistry()
b := NewBroker(broker.Registry(m))
topic := uuid.New().String()
if err := b.Init(); err != nil {
be.Fatalf("Unexpected init error: %v", err)
}
if err := b.Connect(); err != nil {
be.Fatalf("Unexpected connect error: %v", err)
}
msg := &broker.Message{
Header: map[string]string{
"Content-Type": "application/json",
},
Body: []byte(`{"message": "Hello World"}`),
}
done := make(chan bool, c*4)
sub, err := b.Subscribe(topic, func(m *broker.Message) error {
done <- true
if string(m.Body) != string(msg.Body) {
be.Fatalf("Unexpected msg %s, expected %s", string(m.Body), string(msg.Body))
}
return nil
}, broker.Queue("shared"))
if err != nil {
be.Fatalf("Unexpected subscribe error: %v", err)
}
var wg sync.WaitGroup
ch := make(chan int, c*4)
be.StartTimer()
for i := 0; i < c; i++ {
go func() {
for range ch {
if err := b.Publish(topic, msg); err != nil {
be.Fatalf("Unexpected publish error: %v", err)
}
select {
case <-done:
case <-time.After(time.Second):
}
wg.Done()
}
}()
}
for i := 0; i < be.N; i++ {
wg.Add(1)
ch <- i
}
wg.Wait()
be.StopTimer()
sub.Unsubscribe()
close(ch)
close(done)
if err := b.Disconnect(); err != nil {
be.Fatalf("Unexpected disconnect error: %v", err)
}
}
func TestBroker(t *testing.T) {
m := newTestRegistry()
b := NewBroker(broker.Registry(m))
if err := b.Init(); err != nil {
t.Fatalf("Unexpected init error: %v", err)
}
if err := b.Connect(); err != nil {
t.Fatalf("Unexpected connect error: %v", err)
}
msg := &broker.Message{
Header: map[string]string{
"Content-Type": "application/json",
},
Body: []byte(`{"message": "Hello World"}`),
}
done := make(chan bool)
sub, err := b.Subscribe("test", func(m *broker.Message) error {
if string(m.Body) != string(msg.Body) {
t.Fatalf("Unexpected msg %s, expected %s", string(m.Body), string(msg.Body))
}
close(done)
return nil
})
if err != nil {
t.Fatalf("Unexpected subscribe error: %v", err)
}
if err := b.Publish("test", msg); err != nil {
t.Fatalf("Unexpected publish error: %v", err)
}
<-done
sub.Unsubscribe()
if err := b.Disconnect(); err != nil {
t.Fatalf("Unexpected disconnect error: %v", err)
}
}
func TestConcurrentSubBroker(t *testing.T) {
m := newTestRegistry()
b := NewBroker(broker.Registry(m))
if err := b.Init(); err != nil {
t.Fatalf("Unexpected init error: %v", err)
}
if err := b.Connect(); err != nil {
t.Fatalf("Unexpected connect error: %v", err)
}
msg := &broker.Message{
Header: map[string]string{
"Content-Type": "application/json",
},
Body: []byte(`{"message": "Hello World"}`),
}
var subs []broker.Subscriber
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
sub, err := b.Subscribe("test", func(m *broker.Message) error {
defer wg.Done()
if string(m.Body) != string(msg.Body) {
t.Fatalf("Unexpected msg %s, expected %s", string(m.Body), string(msg.Body))
}
return nil
})
if err != nil {
t.Fatalf("Unexpected subscribe error: %v", err)
}
wg.Add(1)
subs = append(subs, sub)
}
if err := b.Publish("test", msg); err != nil {
t.Fatalf("Unexpected publish error: %v", err)
}
wg.Wait()
for _, sub := range subs {
sub.Unsubscribe()
}
if err := b.Disconnect(); err != nil {
t.Fatalf("Unexpected disconnect error: %v", err)
}
}
func TestConcurrentPubBroker(t *testing.T) {
m := newTestRegistry()
b := NewBroker(broker.Registry(m))
if err := b.Init(); err != nil {
t.Fatalf("Unexpected init error: %v", err)
}
if err := b.Connect(); err != nil {
t.Fatalf("Unexpected connect error: %v", err)
}
msg := &broker.Message{
Header: map[string]string{
"Content-Type": "application/json",
},
Body: []byte(`{"message": "Hello World"}`),
}
var wg sync.WaitGroup
sub, err := b.Subscribe("test", func(m *broker.Message) error {
defer wg.Done()
if string(m.Body) != string(msg.Body) {
t.Fatalf("Unexpected msg %s, expected %s", string(m.Body), string(msg.Body))
}
return nil
})
if err != nil {
t.Fatalf("Unexpected subscribe error: %v", err)
}
for i := 0; i < 10; i++ {
wg.Add(1)
if err := b.Publish("test", msg); err != nil {
t.Fatalf("Unexpected publish error: %v", err)
}
}
wg.Wait()
sub.Unsubscribe()
if err := b.Disconnect(); err != nil {
t.Fatalf("Unexpected disconnect error: %v", err)
}
}
func BenchmarkSub1(b *testing.B) {
sub(b, 1)
}
func BenchmarkSub8(b *testing.B) {
sub(b, 8)
}
func BenchmarkSub32(b *testing.B) {
sub(b, 32)
}
func BenchmarkSub64(b *testing.B) {
sub(b, 64)
}
func BenchmarkSub128(b *testing.B) {
sub(b, 128)
}
func BenchmarkPub1(b *testing.B) {
pub(b, 1)
}
func BenchmarkPub8(b *testing.B) {
pub(b, 8)
}
func BenchmarkPub32(b *testing.B) {
pub(b, 32)
}
func BenchmarkPub64(b *testing.B) {
pub(b, 64)
}
func BenchmarkPub128(b *testing.B) {
pub(b, 128)
}

View File

@@ -1,23 +0,0 @@
package http
import (
"context"
"net/http"
"github.com/micro/go-micro/v3/broker"
)
// Handle registers the handler for the given pattern.
func Handle(pattern string, handler http.Handler) broker.Option {
return func(o *broker.Options) {
if o.Context == nil {
o.Context = context.Background()
}
handlers, ok := o.Context.Value("http_handlers").(map[string]http.Handler)
if !ok {
handlers = make(map[string]http.Handler)
}
handlers[pattern] = handler
o.Context = context.WithValue(o.Context, "http_handlers", handlers)
}
}

243
broker/memory.go Normal file
View File

@@ -0,0 +1,243 @@
package broker
import (
"context"
"errors"
"sync"
"github.com/google/uuid"
"github.com/unistack-org/micro/v3/logger"
maddr "github.com/unistack-org/micro/v3/util/addr"
mnet "github.com/unistack-org/micro/v3/util/net"
"github.com/unistack-org/micro/v3/util/rand"
)
type memoryBroker struct {
opts Options
Subscribers map[string][]*memorySubscriber
addr string
sync.RWMutex
connected bool
}
type memoryEvent struct {
opts Options
err error
message interface{}
topic string
}
type memorySubscriber struct {
opts SubscribeOptions
ctx context.Context
exit chan bool
handler Handler
id string
topic string
}
func (m *memoryBroker) Options() Options {
return m.opts
}
func (m *memoryBroker) Address() string {
return m.addr
}
func (m *memoryBroker) Connect(ctx context.Context) error {
m.Lock()
defer m.Unlock()
if m.connected {
return nil
}
// use 127.0.0.1 to avoid scan of all network interfaces
addr, err := maddr.Extract("127.0.0.1")
if err != nil {
return err
}
var rng rand.Rand
i := rng.Intn(20000)
// set addr with port
addr = mnet.HostPort(addr, 10000+i)
m.addr = addr
m.connected = true
return nil
}
func (m *memoryBroker) Disconnect(ctx context.Context) error {
m.Lock()
defer m.Unlock()
if !m.connected {
return nil
}
m.connected = false
return nil
}
func (m *memoryBroker) Init(opts ...Option) error {
for _, o := range opts {
o(&m.opts)
}
return nil
}
func (m *memoryBroker) Publish(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error {
m.RLock()
if !m.connected {
m.RUnlock()
return errors.New("not connected")
}
subs, ok := m.Subscribers[topic]
m.RUnlock()
if !ok {
return nil
}
var v interface{}
if m.opts.Codec != nil {
buf, err := m.opts.Codec.Marshal(msg)
if err != nil {
return err
}
v = buf
} else {
v = msg
}
p := &memoryEvent{
topic: topic,
message: v,
opts: m.opts,
}
eh := m.opts.ErrorHandler
for _, sub := range subs {
if err := sub.handler(p); err != nil {
p.err = err
if sub.opts.ErrorHandler != nil {
eh = sub.opts.ErrorHandler
}
if eh != nil {
eh(p)
} else if m.opts.Logger.V(logger.ErrorLevel) {
m.opts.Logger.Error(m.opts.Context, err.Error())
}
continue
}
}
return nil
}
func (m *memoryBroker) Subscribe(ctx context.Context, topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
m.RLock()
if !m.connected {
m.RUnlock()
return nil, errors.New("not connected")
}
m.RUnlock()
options := NewSubscribeOptions(opts...)
id, err := uuid.NewRandom()
if err != nil {
return nil, err
}
sub := &memorySubscriber{
exit: make(chan bool, 1),
id: id.String(),
topic: topic,
handler: handler,
opts: options,
ctx: ctx,
}
m.Lock()
m.Subscribers[topic] = append(m.Subscribers[topic], sub)
m.Unlock()
go func() {
<-sub.exit
m.Lock()
var newSubscribers []*memorySubscriber
for _, sb := range m.Subscribers[topic] {
if sb.id == sub.id {
continue
}
newSubscribers = append(newSubscribers, sb)
}
m.Subscribers[topic] = newSubscribers
m.Unlock()
}()
return sub, nil
}
func (m *memoryBroker) String() string {
return "memory"
}
func (m *memoryBroker) Name() string {
return m.opts.Name
}
func (m *memoryEvent) Topic() string {
return m.topic
}
func (m *memoryEvent) Message() *Message {
switch v := m.message.(type) {
case *Message:
return v
case []byte:
msg := &Message{}
if err := m.opts.Codec.Unmarshal(v, msg); err != nil {
if m.opts.Logger.V(logger.ErrorLevel) {
m.opts.Logger.Error(m.opts.Context, "[memory]: failed to unmarshal: %v", err)
}
return nil
}
return msg
}
return nil
}
func (m *memoryEvent) Ack() error {
return nil
}
func (m *memoryEvent) Error() error {
return m.err
}
func (m *memorySubscriber) Options() SubscribeOptions {
return m.opts
}
func (m *memorySubscriber) Topic() string {
return m.topic
}
func (m *memorySubscriber) Unsubscribe(ctx context.Context) error {
m.exit <- true
return nil
}
// NewBroker return new memory broker
func NewBroker(opts ...Option) Broker {
return &memoryBroker{
opts: NewOptions(opts...),
Subscribers: make(map[string][]*memorySubscriber),
}
}

View File

@@ -1,183 +0,0 @@
// Package memory provides a memory broker
package memory
import (
"context"
"errors"
"math/rand"
"sync"
"time"
"github.com/google/uuid"
"github.com/micro/go-micro/v3/broker"
maddr "github.com/micro/go-micro/v3/util/addr"
mnet "github.com/micro/go-micro/v3/util/net"
)
type memoryBroker struct {
opts broker.Options
addr string
sync.RWMutex
connected bool
Subscribers map[string][]*memorySubscriber
}
type memorySubscriber struct {
id string
topic string
exit chan bool
handler broker.Handler
opts broker.SubscribeOptions
}
func (m *memoryBroker) Options() broker.Options {
return m.opts
}
func (m *memoryBroker) Address() string {
return m.addr
}
func (m *memoryBroker) Connect() error {
m.Lock()
defer m.Unlock()
if m.connected {
return nil
}
// use 127.0.0.1 to avoid scan of all network interfaces
addr, err := maddr.Extract("127.0.0.1")
if err != nil {
return err
}
i := rand.Intn(20000)
// set addr with port
addr = mnet.HostPort(addr, 10000+i)
m.addr = addr
m.connected = true
return nil
}
func (m *memoryBroker) Disconnect() error {
m.Lock()
defer m.Unlock()
if !m.connected {
return nil
}
m.connected = false
return nil
}
func (m *memoryBroker) Init(opts ...broker.Option) error {
for _, o := range opts {
o(&m.opts)
}
return nil
}
func (m *memoryBroker) Publish(topic string, msg *broker.Message, opts ...broker.PublishOption) error {
m.RLock()
if !m.connected {
m.RUnlock()
return errors.New("not connected")
}
subs, ok := m.Subscribers[topic]
m.RUnlock()
if !ok {
return nil
}
for _, sub := range subs {
if err := sub.handler(msg); err != nil {
if eh := sub.opts.ErrorHandler; eh != nil {
eh(msg, err)
}
continue
}
}
return nil
}
func (m *memoryBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
m.RLock()
if !m.connected {
m.RUnlock()
return nil, errors.New("not connected")
}
m.RUnlock()
var options broker.SubscribeOptions
for _, o := range opts {
o(&options)
}
sub := &memorySubscriber{
exit: make(chan bool, 1),
id: uuid.New().String(),
topic: topic,
handler: handler,
opts: options,
}
m.Lock()
m.Subscribers[topic] = append(m.Subscribers[topic], sub)
m.Unlock()
go func() {
<-sub.exit
m.Lock()
var newSubscribers []*memorySubscriber
for _, sb := range m.Subscribers[topic] {
if sb.id == sub.id {
continue
}
newSubscribers = append(newSubscribers, sb)
}
m.Subscribers[topic] = newSubscribers
m.Unlock()
}()
return sub, nil
}
func (m *memoryBroker) String() string {
return "memory"
}
func (m *memorySubscriber) Options() broker.SubscribeOptions {
return m.opts
}
func (m *memorySubscriber) Topic() string {
return m.topic
}
func (m *memorySubscriber) Unsubscribe() error {
m.exit <- true
return nil
}
func NewBroker(opts ...broker.Option) broker.Broker {
options := broker.Options{
Context: context.Background(),
}
rand.Seed(time.Now().UnixNano())
for _, o := range opts {
o(&options)
}
return &memoryBroker{
opts: options,
Subscribers: make(map[string][]*memorySubscriber),
}
}

View File

@@ -1,33 +1,33 @@
package memory
package broker
import (
"context"
"fmt"
"testing"
"github.com/micro/go-micro/v3/broker"
)
func TestMemoryBroker(t *testing.T) {
b := NewBroker()
ctx := context.Background()
if err := b.Connect(); err != nil {
if err := b.Connect(ctx); err != nil {
t.Fatalf("Unexpected connect error %v", err)
}
topic := "test"
count := 10
fn := func(m *broker.Message) error {
fn := func(p Event) error {
return nil
}
sub, err := b.Subscribe(topic, fn)
sub, err := b.Subscribe(ctx, topic, fn)
if err != nil {
t.Fatalf("Unexpected error subscribing %v", err)
}
for i := 0; i < count; i++ {
message := &broker.Message{
message := &Message{
Header: map[string]string{
"foo": "bar",
"id": fmt.Sprintf("%d", i),
@@ -35,16 +35,16 @@ func TestMemoryBroker(t *testing.T) {
Body: []byte(`hello world`),
}
if err := b.Publish(topic, message); err != nil {
if err := b.Publish(ctx, topic, message); err != nil {
t.Fatalf("Unexpected error publishing %d", i)
}
}
if err := sub.Unsubscribe(); err != nil {
if err := sub.Unsubscribe(ctx); err != nil {
t.Fatalf("Unexpected error unsubscribing from %s: %v", topic, err)
}
if err := b.Disconnect(); err != nil {
if err := b.Disconnect(ctx); err != nil {
t.Fatalf("Unexpected connect error %v", err)
}
}

View File

@@ -1,17 +0,0 @@
package nats
import (
"context"
"github.com/micro/go-micro/v3/broker"
)
// setBrokerOption returns a function to setup a context with given value
func setBrokerOption(k, v interface{}) broker.Option {
return func(o *broker.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}

View File

@@ -1,294 +0,0 @@
// Package nats provides a NATS broker
package nats
import (
"context"
"errors"
"strings"
"sync"
"github.com/micro/go-micro/v3/broker"
"github.com/micro/go-micro/v3/codec/json"
"github.com/micro/go-micro/v3/logger"
"github.com/micro/go-micro/v3/registry/mdns"
nats "github.com/nats-io/nats.go"
)
type natsBroker struct {
sync.Once
sync.RWMutex
// indicate if we're connected
connected bool
addrs []string
conn *nats.Conn
opts broker.Options
nopts nats.Options
// should we drain the connection
drain bool
closeCh chan (error)
}
type subscriber struct {
s *nats.Subscription
opts broker.SubscribeOptions
}
func (s *subscriber) Options() broker.SubscribeOptions {
return s.opts
}
func (s *subscriber) Topic() string {
return s.s.Subject
}
func (s *subscriber) Unsubscribe() error {
return s.s.Unsubscribe()
}
func (n *natsBroker) Address() string {
if n.conn != nil && n.conn.IsConnected() {
return n.conn.ConnectedUrl()
}
if len(n.addrs) > 0 {
return n.addrs[0]
}
return ""
}
func (n *natsBroker) setAddrs(addrs []string) []string {
//nolint:prealloc
var cAddrs []string
for _, addr := range addrs {
if len(addr) == 0 {
continue
}
if !strings.HasPrefix(addr, "nats://") {
addr = "nats://" + addr
}
cAddrs = append(cAddrs, addr)
}
if len(cAddrs) == 0 {
cAddrs = []string{nats.DefaultURL}
}
return cAddrs
}
func (n *natsBroker) Connect() error {
n.Lock()
defer n.Unlock()
if n.connected {
return nil
}
status := nats.CLOSED
if n.conn != nil {
status = n.conn.Status()
}
switch status {
case nats.CONNECTED, nats.RECONNECTING, nats.CONNECTING:
n.connected = true
return nil
default: // DISCONNECTED or CLOSED or DRAINING
opts := n.nopts
opts.Servers = n.addrs
opts.Secure = n.opts.Secure
opts.TLSConfig = n.opts.TLSConfig
// secure might not be set
if n.opts.TLSConfig != nil {
opts.Secure = true
}
c, err := opts.Connect()
if err != nil {
if logger.V(logger.WarnLevel, logger.DefaultLogger) {
logger.Warnf("Error connecting to broker: %v", err)
}
return err
}
n.conn = c
n.connected = true
return nil
}
}
func (n *natsBroker) Disconnect() error {
n.Lock()
defer n.Unlock()
// drain the connection if specified
if n.drain {
n.conn.Drain()
n.closeCh <- nil
}
// close the client connection
n.conn.Close()
// set not connected
n.connected = false
return nil
}
func (n *natsBroker) Init(opts ...broker.Option) error {
n.setOption(opts...)
return nil
}
func (n *natsBroker) Options() broker.Options {
return n.opts
}
func (n *natsBroker) Publish(topic string, msg *broker.Message, opts ...broker.PublishOption) error {
n.RLock()
defer n.RUnlock()
if n.conn == nil {
return errors.New("not connected")
}
b, err := n.opts.Codec.Marshal(msg)
if err != nil {
return err
}
return n.conn.Publish(topic, b)
}
func (n *natsBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
n.RLock()
if n.conn == nil {
n.RUnlock()
return nil, errors.New("not connected")
}
n.RUnlock()
opt := broker.SubscribeOptions{
Context: context.Background(),
}
for _, o := range opts {
o(&opt)
}
fn := func(msg *nats.Msg) {
var m *broker.Message
eh := opt.ErrorHandler
err := n.opts.Codec.Unmarshal(msg.Data, &m)
if err != nil {
m.Body = msg.Data
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(err)
}
if eh != nil {
eh(m, err)
}
return
}
if err := handler(m); err != nil {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(err)
}
if eh != nil {
eh(m, err)
}
}
}
var sub *nats.Subscription
var err error
n.RLock()
if len(opt.Queue) > 0 {
sub, err = n.conn.QueueSubscribe(topic, opt.Queue, fn)
} else {
sub, err = n.conn.Subscribe(topic, fn)
}
n.RUnlock()
if err != nil {
return nil, err
}
return &subscriber{s: sub, opts: opt}, nil
}
func (n *natsBroker) String() string {
return "nats"
}
func (n *natsBroker) setOption(opts ...broker.Option) {
for _, o := range opts {
o(&n.opts)
}
n.Once.Do(func() {
n.nopts = nats.GetDefaultOptions()
})
if nopts, ok := n.opts.Context.Value(optionsKey{}).(nats.Options); ok {
n.nopts = nopts
}
// broker.Options have higher priority than nats.Options
// only if Addrs, Secure or TLSConfig were not set through a broker.Option
// we read them from nats.Option
if len(n.opts.Addrs) == 0 {
n.opts.Addrs = n.nopts.Servers
}
if !n.opts.Secure {
n.opts.Secure = n.nopts.Secure
}
if n.opts.TLSConfig == nil {
n.opts.TLSConfig = n.nopts.TLSConfig
}
n.addrs = n.setAddrs(n.opts.Addrs)
if n.opts.Context.Value(drainConnectionKey{}) != nil {
n.drain = true
n.closeCh = make(chan error)
n.nopts.ClosedCB = n.onClose
n.nopts.AsyncErrorCB = n.onAsyncError
n.nopts.DisconnectedErrCB = n.onDisconnectedError
}
}
func (n *natsBroker) onClose(conn *nats.Conn) {
n.closeCh <- nil
}
func (n *natsBroker) onAsyncError(conn *nats.Conn, sub *nats.Subscription, err error) {
// There are kinds of different async error nats might callback, but we are interested
// in ErrDrainTimeout only here.
if err == nats.ErrDrainTimeout {
n.closeCh <- err
}
}
func (n *natsBroker) onDisconnectedError(conn *nats.Conn, err error) {
n.closeCh <- err
}
func NewBroker(opts ...broker.Option) broker.Broker {
options := broker.Options{
// Default codec
Codec: json.Marshaler{},
Context: context.Background(),
Registry: mdns.NewRegistry(),
}
n := &natsBroker{
opts: options,
}
n.setOption(opts...)
return n
}

View File

@@ -1,98 +0,0 @@
package nats
import (
"fmt"
"testing"
"github.com/micro/go-micro/v3/broker"
nats "github.com/nats-io/nats.go"
)
var addrTestCases = []struct {
name string
description string
addrs map[string]string // expected address : set address
}{
{
"brokerOpts",
"set broker addresses through a broker.Option in constructor",
map[string]string{
"nats://192.168.10.1:5222": "192.168.10.1:5222",
"nats://10.20.10.0:4222": "10.20.10.0:4222"},
},
{
"brokerInit",
"set broker addresses through a broker.Option in broker.Init()",
map[string]string{
"nats://192.168.10.1:5222": "192.168.10.1:5222",
"nats://10.20.10.0:4222": "10.20.10.0:4222"},
},
{
"natsOpts",
"set broker addresses through the nats.Option in constructor",
map[string]string{
"nats://192.168.10.1:5222": "192.168.10.1:5222",
"nats://10.20.10.0:4222": "10.20.10.0:4222"},
},
{
"default",
"check if default Address is set correctly",
map[string]string{
"nats://127.0.0.1:4222": "",
},
},
}
// TestInitAddrs tests issue #100. Ensures that if the addrs is set by an option in init it will be used.
func TestInitAddrs(t *testing.T) {
for _, tc := range addrTestCases {
t.Run(fmt.Sprintf("%s: %s", tc.name, tc.description), func(t *testing.T) {
var br broker.Broker
var addrs []string
for _, addr := range tc.addrs {
addrs = append(addrs, addr)
}
switch tc.name {
case "brokerOpts":
// we know that there are just two addrs in the dict
br = NewBroker(broker.Addrs(addrs[0], addrs[1]))
br.Init()
case "brokerInit":
br = NewBroker()
// we know that there are just two addrs in the dict
br.Init(broker.Addrs(addrs[0], addrs[1]))
case "natsOpts":
nopts := nats.GetDefaultOptions()
nopts.Servers = addrs
br = NewBroker(Options(nopts))
br.Init()
case "default":
br = NewBroker()
br.Init()
}
natsBroker, ok := br.(*natsBroker)
if !ok {
t.Fatal("Expected broker to be of types *natsBroker")
}
// check if the same amount of addrs we set has actually been set, default
// have only 1 address nats://127.0.0.1:4222 (current nats code) or
// nats://localhost:4222 (older code version)
if len(natsBroker.addrs) != len(tc.addrs) && tc.name != "default" {
t.Errorf("Expected Addr count = %d, Actual Addr count = %d",
len(natsBroker.addrs), len(tc.addrs))
}
for _, addr := range natsBroker.addrs {
_, ok := tc.addrs[addr]
if !ok {
t.Errorf("Expected '%s' has not been set", addr)
}
}
})
}
}

View File

@@ -1,19 +0,0 @@
package nats
import (
"github.com/micro/go-micro/v3/broker"
nats "github.com/nats-io/nats.go"
)
type optionsKey struct{}
type drainConnectionKey struct{}
// Options accepts nats.Options
func Options(opts nats.Options) broker.Option {
return setBrokerOption(optionsKey{}, opts)
}
// DrainConnection will drain subscription on close
func DrainConnection() broker.Option {
return setBrokerOption(drainConnectionKey{}, struct{}{})
}

View File

@@ -4,64 +4,130 @@ import (
"context"
"crypto/tls"
"github.com/micro/go-micro/v3/codec"
"github.com/micro/go-micro/v3/registry"
"github.com/unistack-org/micro/v3/codec"
"github.com/unistack-org/micro/v3/logger"
"github.com/unistack-org/micro/v3/meter"
"github.com/unistack-org/micro/v3/register"
"github.com/unistack-org/micro/v3/tracer"
)
// Options struct
type Options struct {
Addrs []string
Secure bool
Codec codec.Marshaler
// Tracer used for tracing
Tracer tracer.Tracer
// Register can be used for clustering
Register register.Register
// Codec holds the codec for marshal/unmarshal
Codec codec.Codec
// Logger used for logging
Logger logger.Logger
// Meter used for metrics
Meter meter.Meter
// Context holds external options
Context context.Context
// TLSConfig holds tls.TLSConfig options
TLSConfig *tls.Config
// Registry used for clustering
Registry registry.Registry
// Other options for implementations of the interface
// can be stored in a context
Context context.Context
// ErrorHandler used when broker can't unmarshal incoming message
ErrorHandler Handler
// Name holds the broker name
Name string
// Addrs holds the broker address
Addrs []string
}
// NewOptions create new Options
func NewOptions(opts ...Option) Options {
options := Options{
Register: register.DefaultRegister,
Logger: logger.DefaultLogger,
Context: context.Background(),
Meter: meter.DefaultMeter,
Codec: codec.DefaultCodec,
Tracer: tracer.DefaultTracer,
}
for _, o := range opts {
o(&options)
}
return options
}
// Context sets the context option
func Context(ctx context.Context) Option {
return func(o *Options) {
o.Context = ctx
}
}
// PublishOptions struct
type PublishOptions struct {
// Other options for implementations of the interface
// can be stored in a context
// Context holds external options
Context context.Context
// BodyOnly flag says the message contains raw body bytes
BodyOnly bool
}
// NewPublishOptions creates PublishOptions struct
func NewPublishOptions(opts ...PublishOption) PublishOptions {
options := PublishOptions{
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
return options
}
// SubscribeOptions struct
type SubscribeOptions struct {
// Handler executed when errors occur processing messages
ErrorHandler ErrorHandler
// Subscribers with the same queue name
// will create a shared subscription where each
// receives a subset of messages.
Queue string
// Other options for implementations of the interface
// can be stored in a context
// Context holds external options
Context context.Context
// ErrorHandler used when broker can't unmarshal incoming message
ErrorHandler Handler
// Group holds consumer group
Group string
// AutoAck flag specifies auto ack of incoming message when no error happens
AutoAck bool
// BodyOnly flag specifies that message contains only body bytes without header
BodyOnly bool
}
// Option func
type Option func(*Options)
// PublishOption func
type PublishOption func(*PublishOptions)
// PublishContext set context
// PublishBodyOnly publish only body of the message
func PublishBodyOnly(b bool) PublishOption {
return func(o *PublishOptions) {
o.BodyOnly = b
}
}
// PublishContext sets the context
func PublishContext(ctx context.Context) PublishOption {
return func(o *PublishOptions) {
o.Context = ctx
}
}
// SubscribeOption func
type SubscribeOption func(*SubscribeOptions)
// NewSubscribeOptions creates new SubscribeOptions
func NewSubscribeOptions(opts ...SubscribeOption) SubscribeOptions {
opt := SubscribeOptions{}
for _, o := range opts {
o(&opt)
options := SubscribeOptions{
AutoAck: true,
Context: context.Background(),
}
return opt
for _, o := range opts {
o(&options)
}
return options
}
// Addrs sets the host addresses to be used by the broker
@@ -73,47 +139,107 @@ func Addrs(addrs ...string) Option {
// Codec sets the codec used for encoding/decoding used where
// a broker does not support headers
func Codec(c codec.Marshaler) Option {
func Codec(c codec.Codec) Option {
return func(o *Options) {
o.Codec = c
}
}
// DisableAutoAck disables auto ack
func DisableAutoAck() SubscribeOption {
return func(o *SubscribeOptions) {
o.AutoAck = false
}
}
// SubscribeAutoAck will disable auto acking of messages
// after they have been handled.
func SubscribeAutoAck(b bool) SubscribeOption {
return func(o *SubscribeOptions) {
o.AutoAck = b
}
}
// SubscribeBodyOnly consumes only body of the message
func SubscribeBodyOnly(b bool) SubscribeOption {
return func(o *SubscribeOptions) {
o.BodyOnly = b
}
}
// ErrorHandler will catch all broker errors that cant be handled
// in normal way, for example Codec errors
func HandleError(h ErrorHandler) SubscribeOption {
func ErrorHandler(h Handler) Option {
return func(o *Options) {
o.ErrorHandler = h
}
}
// SubscribeErrorHandler will catch all broker errors that cant be handled
// in normal way, for example Codec errors
func SubscribeErrorHandler(h Handler) SubscribeOption {
return func(o *SubscribeOptions) {
o.ErrorHandler = h
}
}
// Queue sets the name of the queue to share messages on
// Queue sets the subscribers queue
// Deprecated
func Queue(name string) SubscribeOption {
return func(o *SubscribeOptions) {
o.Queue = name
o.Group = name
}
}
func Registry(r registry.Registry) Option {
// SubscribeGroup sets the name of the queue to share messages on
func SubscribeGroup(name string) SubscribeOption {
return func(o *SubscribeOptions) {
o.Group = name
}
}
// Register sets register option
func Register(r register.Register) Option {
return func(o *Options) {
o.Registry = r
o.Register = r
}
}
// Secure communication with the broker
func Secure(b bool) Option {
return func(o *Options) {
o.Secure = b
}
}
// Specify TLS Config
// TLSConfig sets the TLS Config
func TLSConfig(t *tls.Config) Option {
return func(o *Options) {
o.TLSConfig = t
}
}
// Logger sets the logger
func Logger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// Tracer to be used for tracing
func Tracer(t tracer.Tracer) Option {
return func(o *Options) {
o.Tracer = t
}
}
// Meter sets the meter
func Meter(m meter.Meter) Option {
return func(o *Options) {
o.Meter = m
}
}
// Name sets the name
func Name(n string) Option {
return func(o *Options) {
o.Name = n
}
}
// SubscribeContext set context
func SubscribeContext(ctx context.Context) SubscribeOption {
return func(o *SubscribeOptions) {

View File

@@ -21,12 +21,12 @@ type Source struct {
// Package is packaged format for source
type Package struct {
// Source of the package
Source *Source
// Name of the package
Name string
// Location of the package
Path string
// Type of package e.g tarball, binary, docker
Type string
// Source of the package
Source *Source
}

View File

@@ -1,96 +0,0 @@
// Package docker builds docker images
package docker
import (
"archive/tar"
"bytes"
"io/ioutil"
"os"
"path/filepath"
docker "github.com/fsouza/go-dockerclient"
"github.com/micro/go-micro/v3/build"
"github.com/micro/go-micro/v3/logger"
)
type dockerBuild struct {
Options build.Options
Client *docker.Client
}
func (d *dockerBuild) Package(name string, s *build.Source) (*build.Package, error) {
image := name
buf := new(bytes.Buffer)
tw := tar.NewWriter(buf)
defer tw.Close()
dockerFile := "Dockerfile"
// open docker file
f, err := os.Open(filepath.Join(s.Path, dockerFile))
if err != nil {
return nil, err
}
// read docker file
by, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
tarHeader := &tar.Header{
Name: dockerFile,
Size: int64(len(by)),
}
err = tw.WriteHeader(tarHeader)
if err != nil {
return nil, err
}
_, err = tw.Write(by)
if err != nil {
return nil, err
}
tr := bytes.NewReader(buf.Bytes())
err = d.Client.BuildImage(docker.BuildImageOptions{
Name: image,
Dockerfile: dockerFile,
InputStream: tr,
OutputStream: ioutil.Discard,
RmTmpContainer: true,
SuppressOutput: true,
})
if err != nil {
return nil, err
}
return &build.Package{
Name: image,
Path: image,
Type: "docker",
Source: s,
}, nil
}
func (d *dockerBuild) Remove(b *build.Package) error {
return d.Client.RemoveImage(b.Name)
}
func (d *dockerBuild) String() string {
return "docker"
}
func NewBuild(opts ...build.Option) build.Build {
options := build.Options{}
for _, o := range opts {
o(&options)
}
endpoint := "unix:///var/run/docker.sock"
client, err := docker.NewClient(endpoint)
if err != nil {
logger.Fatal(err)
}
return &dockerBuild{
Options: options,
Client: client,
}
}

View File

@@ -1,75 +0,0 @@
// Package golang is a go package manager
package golang
import (
"os"
"os/exec"
"path/filepath"
"github.com/micro/go-micro/v3/build"
)
type goBuild struct {
Options build.Options
Cmd string
Path string
}
// whichGo locates the go command
func whichGo() string {
// check GOROOT
if gr := os.Getenv("GOROOT"); len(gr) > 0 {
return filepath.Join(gr, "bin", "go")
}
// check path
for _, p := range filepath.SplitList(os.Getenv("PATH")) {
bin := filepath.Join(p, "go")
if _, err := os.Stat(bin); err == nil {
return bin
}
}
// best effort
return "go"
}
func (g *goBuild) Package(name string, src *build.Source) (*build.Package, error) {
binary := filepath.Join(g.Path, name)
source := src.Path
cmd := exec.Command(g.Cmd, "build", "-o", binary, source)
if err := cmd.Run(); err != nil {
return nil, err
}
return &build.Package{
Name: name,
Path: binary,
Type: g.String(),
Source: src,
}, nil
}
func (g *goBuild) Remove(b *build.Package) error {
binary := filepath.Join(b.Path, b.Name)
return os.Remove(binary)
}
func (g *goBuild) String() string {
return "golang"
}
func NewBuild(opts ...build.Option) build.Build {
options := build.Options{
Path: os.TempDir(),
}
for _, o := range opts {
o(&options)
}
return &goBuild{
Options: options,
Cmd: whichGo(),
Path: options.Path,
}
}

View File

@@ -1,13 +1,15 @@
package build
// Options struct
type Options struct {
// local path to download source
Path string
}
// Option func
type Option func(o *Options)
// Local path for repository
// Path is the Local path for repository
func Path(p string) Option {
return func(o *Options) {
o.Path = p

View File

@@ -1,45 +0,0 @@
// Package tar basically tarballs source code
package tar
import (
"os"
"path/filepath"
"github.com/micro/go-micro/v3/build"
)
type tarBuild struct{}
func (t *tarBuild) Package(name string, src *build.Source) (*build.Package, error) {
pkg := name + ".tar.gz"
// path to the tarball
path := filepath.Join(os.TempDir(), src.Path, pkg)
// create a temp directory
if err := os.MkdirAll(filepath.Join(os.TempDir(), src.Path), 0755); err != nil {
return nil, err
}
if err := Compress(src.Path, path); err != nil {
return nil, err
}
return &build.Package{
Name: name,
Path: path,
Type: t.String(),
Source: src,
}, nil
}
func (t *tarBuild) Remove(b *build.Package) error {
return os.Remove(b.Path)
}
func (t *tarBuild) String() string {
return "tar.gz"
}
func NewBuild(opts ...build.Option) build.Build {
return new(tarBuild)
}

View File

@@ -1,92 +0,0 @@
package tar
import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"os"
"path/filepath"
"strings"
)
func Compress(source, dest string) error {
// tar + gzip
var buf bytes.Buffer
_ = compress(source, &buf)
// write the .tar.gzip
fileToWrite, err := os.OpenFile(dest, os.O_CREATE|os.O_RDWR, 0666)
if err != nil {
return err
}
_, err = io.Copy(fileToWrite, &buf)
return err
}
func compress(src string, buf io.Writer) error {
// tar > gzip > buf
zr := gzip.NewWriter(buf)
tw := tar.NewWriter(zr)
// walk through every file in the folder
filepath.Walk(src, func(file string, fi os.FileInfo, err error) error {
// generate tar header
header, err := tar.FileInfoHeader(fi, file)
if err != nil {
return err
}
// must provide real name
// (see https://golang.org/src/archive/tar/common.go?#L626)
srcWithSlash := src
if !strings.HasSuffix(src, string(filepath.Separator)) {
srcWithSlash = src + string(filepath.Separator)
}
header.Name = strings.ReplaceAll(file, srcWithSlash, "")
if header.Name == src || len(strings.TrimSpace(header.Name)) == 0 {
return nil
}
// @todo This is a quick hack to speed up whole repo uploads
// https://github.com/micro/micro/pull/956
if !fi.IsDir() && !strings.HasSuffix(header.Name, ".go") &&
!strings.HasSuffix(header.Name, ".mod") &&
!strings.HasSuffix(header.Name, ".sum") {
return nil
}
// write header
if err := tw.WriteHeader(header); err != nil {
return err
}
if fi.IsDir() {
return nil
}
// if not a dir, write file content
data, err := os.Open(file)
if err != nil {
return err
}
if _, err := io.Copy(tw, data); err != nil {
return err
}
return nil
})
// produce tar
if err := tw.Close(); err != nil {
return err
}
// produce gzip
if err := zr.Close(); err != nil {
return err
}
//
return nil
}

29
cache/cache.go vendored
View File

@@ -1,29 +0,0 @@
// Package cache is a caching interface
package cache
// Cache is an interface for caching
type Cache interface {
// Initialise options
Init(...Option) error
// Get a value
Get(key string) (interface{}, error)
// Set a value
Set(key string, val interface{}) error
// Delete a value
Delete(key string) error
// Name of the implementation
String() string
}
type Options struct {
Nodes []string
}
type Option func(o *Options)
// Nodes sets the nodes for the cache
func Nodes(v ...string) Option {
return func(o *Options) {
o.Nodes = v
}
}

View File

@@ -1,80 +0,0 @@
// Package memcache is a memcache implementation of the Cache
package memcache
import (
"encoding/json"
"github.com/bradfitz/gomemcache/memcache"
"github.com/micro/go-micro/v3/cache"
)
type memcacheCache struct {
options cache.Options
client *memcache.Client
}
type memcacheItem struct {
Key string
Value interface{}
}
func (m *memcacheCache) Init(opts ...cache.Option) error {
for _, o := range opts {
o(&m.options)
}
return nil
}
func (m *memcacheCache) Get(key string) (interface{}, error) {
item, err := m.client.Get(key)
if err != nil {
return nil, err
}
var mc *memcacheItem
if err := json.Unmarshal(item.Value, &mc); err != nil {
return nil, err
}
return mc.Value, nil
}
func (m *memcacheCache) Set(key string, val interface{}) error {
b, err := json.Marshal(val)
if err != nil {
return err
}
return m.client.Set(&memcache.Item{
Key: key,
Value: b,
})
}
func (m *memcacheCache) Delete(key string) error {
return m.client.Delete(key)
}
func (m *memcacheCache) String() string {
return "memcache"
}
// NewCache returns a new memcache Cache
func NewCache(opts ...cache.Option) cache.Cache {
var options cache.Options
for _, o := range opts {
o(&options)
}
// get and set the nodes
nodes := options.Nodes
if len(nodes) == 0 {
nodes = []string{"localhost:11211"}
}
return &memcacheCache{
options: options,
client: memcache.New(nodes...),
}
}

View File

@@ -1,56 +0,0 @@
// Package memory is an in memory cache
package memory
import (
"sync"
"github.com/micro/go-micro/v3/cache"
"github.com/micro/go-micro/v3/errors"
)
type memoryCache struct {
// TODO: use a decent caching library
sync.RWMutex
values map[string]interface{}
}
func (m *memoryCache) Init(opts ...cache.Option) error {
// TODO: implement
return nil
}
func (m *memoryCache) Get(key string) (interface{}, error) {
m.RLock()
defer m.RUnlock()
v, ok := m.values[key]
if !ok {
return nil, errors.NotFound("go.micro.cache", key+" not found")
}
return v, nil
}
func (m *memoryCache) Set(key string, val interface{}) error {
m.Lock()
m.values[key] = val
m.Unlock()
return nil
}
func (m *memoryCache) Delete(key string) error {
m.Lock()
delete(m.values, key)
m.Unlock()
return nil
}
func (m *memoryCache) String() string {
return "memory"
}
func NewCache(opts ...cache.Option) cache.Cache {
return &memoryCache{
values: make(map[string]interface{}),
}
}

View File

@@ -4,9 +4,10 @@ import (
"context"
"time"
"github.com/micro/go-micro/v3/util/backoff"
"github.com/unistack-org/micro/v3/util/backoff"
)
// BackoffFunc is the backoff call func
type BackoffFunc func(ctx context.Context, req Request, attempts int) (time.Duration, error)
func exponentialBackoff(ctx context.Context, req Request, attempts int) (time.Duration, error) {

View File

@@ -4,8 +4,6 @@ import (
"context"
"testing"
"time"
"github.com/micro/go-micro/v3/codec"
)
func TestBackoff(t *testing.T) {
@@ -34,63 +32,3 @@ func TestBackoff(t *testing.T) {
}
}
}
type testRequest struct {
service string
method string
endpoint string
contentType string
codec codec.Codec
body interface{}
opts RequestOptions
}
func newRequest(service, endpoint string, request interface{}, contentType string, reqOpts ...RequestOption) Request {
var opts RequestOptions
for _, o := range reqOpts {
o(&opts)
}
// set the content-type specified
if len(opts.ContentType) > 0 {
contentType = opts.ContentType
}
return &testRequest{
service: service,
method: endpoint,
endpoint: endpoint,
body: request,
contentType: contentType,
opts: opts,
}
}
func (r *testRequest) ContentType() string {
return r.contentType
}
func (r *testRequest) Service() string {
return r.service
}
func (r *testRequest) Method() string {
return r.method
}
func (r *testRequest) Endpoint() string {
return r.endpoint
}
func (r *testRequest) Body() interface{} {
return r.body
}
func (r *testRequest) Codec() codec.Writer {
return r.codec
}
func (r *testRequest) Stream() bool {
return r.opts.Stream
}

View File

@@ -5,17 +5,38 @@ import (
"context"
"time"
"github.com/micro/go-micro/v3/codec"
"github.com/unistack-org/micro/v3/codec"
"github.com/unistack-org/micro/v3/metadata"
)
var (
// DefaultClient is the global default client
DefaultClient Client = NewClient()
// DefaultContentType is the default content-type if not specified
DefaultContentType = "application/json"
// DefaultBackoff is the default backoff function for retries
DefaultBackoff = exponentialBackoff
// DefaultRetry is the default check-for-retry function for retries
DefaultRetry = RetryNever
// DefaultRetries is the default number of times a request is tried
DefaultRetries = 0
// DefaultRequestTimeout is the default request timeout
DefaultRequestTimeout = time.Second * 5
// DefaultPoolSize sets the connection pool size
DefaultPoolSize = 100
// DefaultPoolTTL sets the connection pool ttl
DefaultPoolTTL = time.Minute
)
// Client is the interface used to make requests to services.
// It supports Request/Response via Transport and Publishing via the Broker.
// It also supports bidirectional streaming of requests.
type Client interface {
Init(...Option) error
Name() string
Init(opts ...Option) error
Options() Options
NewMessage(topic string, msg interface{}, opts ...MessageOption) Message
NewRequest(service, endpoint string, req interface{}, reqOpts ...RequestOption) Request
NewRequest(service string, endpoint string, req interface{}, opts ...RequestOption) Request
Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
Publish(ctx context.Context, msg Message, opts ...PublishOption) error
@@ -42,7 +63,7 @@ type Request interface {
// The unencoded request body
Body() interface{}
// Write to the encoded request writer. This is nil before a call is made
Codec() codec.Writer
Codec() codec.Codec
// indicates whether the request will be a streaming one rather than unary
Stream() bool
}
@@ -50,9 +71,9 @@ type Request interface {
// Response is the response received from a service
type Response interface {
// Read the response
Codec() codec.Reader
Codec() codec.Codec
// read the header
Header() map[string]string
Header() metadata.Metadata
// Read the undecoded response
Read() ([]byte, error)
}
@@ -66,9 +87,9 @@ type Stream interface {
// The response read
Response() Response
// Send will encode and send a request
Send(interface{}) error
Send(msg interface{}) error
// Recv will decode and read a response
Recv(interface{}) error
Recv(msg interface{}) error
// Error returns the stream error
Error() error
// Close closes the stream
@@ -89,18 +110,3 @@ type MessageOption func(*MessageOptions)
// RequestOption used by NewRequest
type RequestOption func(*RequestOptions)
var (
// DefaultBackoff is the default backoff function for retries
DefaultBackoff = exponentialBackoff
// DefaultRetry is the default check-for-retry function for retries
DefaultRetry = RetryOnError
// DefaultRetries is the default number of times a request is tried
DefaultRetries = 1
// DefaultRequestTimeout is the default request timeout
DefaultRequestTimeout = time.Second * 5
// DefaultPoolSize sets the connection pool size
DefaultPoolSize = 100
// DefaultPoolTTL sets the connection pool ttl
DefaultPoolTTL = time.Minute
)

View File

@@ -0,0 +1,23 @@
package client
import (
"context"
)
type clientCallOptions struct {
Client
opts []CallOption
}
func (s *clientCallOptions) Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error {
return s.Client.Call(ctx, req, rsp, append(s.opts, opts...)...)
}
func (s *clientCallOptions) Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error) {
return s.Client.Stream(ctx, req, append(s.opts, opts...)...)
}
// NewClientCallOptions add CallOption to every call
func NewClientCallOptions(c Client, opts ...CallOption) Client {
return &clientCallOptions{c, opts}
}

54
client/context.go Normal file
View File

@@ -0,0 +1,54 @@
package client
import (
"context"
)
type clientKey struct{}
// FromContext get client from context
func FromContext(ctx context.Context) (Client, bool) {
if ctx == nil {
return nil, false
}
c, ok := ctx.Value(clientKey{}).(Client)
return c, ok
}
// NewContext put client in context
func NewContext(ctx context.Context, c Client) context.Context {
if ctx == nil {
ctx = context.Background()
}
return context.WithValue(ctx, clientKey{}, c)
}
// SetPublishOption returns a function to setup a context with given value
func SetPublishOption(k, v interface{}) PublishOption {
return func(o *PublishOptions) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}
// SetCallOption returns a function to setup a context with given value
func SetCallOption(k, v interface{}) CallOption {
return func(o *CallOptions) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}
// SetOption returns a function to setup a context with given value
func SetOption(k, v interface{}) Option {
return func(o *Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}

View File

@@ -1,25 +0,0 @@
# GRPC Client
The grpc client is a [micro.Client](https://godoc.org/github.com/micro/go-micro/client#Client) compatible client.
## Overview
The client makes use of the [google.golang.org/grpc](google.golang.org/grpc) framework for the underlying communication mechanism.
## Usage
Specify the client to your micro service
```go
import (
"github.com/micro/go-micro"
"github.com/micro/go-plugins/client/grpc"
)
func main() {
service := micro.NewService(
micro.Name("greeter"),
micro.Client(grpc.NewClient()),
)
}
```

View File

@@ -1,206 +0,0 @@
package grpc
import (
"encoding/json"
"fmt"
"strings"
b "bytes"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"github.com/micro/go-micro/v3/codec"
"github.com/micro/go-micro/v3/codec/bytes"
"github.com/oxtoacart/bpool"
"google.golang.org/grpc"
"google.golang.org/grpc/encoding"
)
type jsonCodec struct{}
type protoCodec struct{}
type bytesCodec struct{}
type wrapCodec struct{ encoding.Codec }
var jsonpbMarshaler = &jsonpb.Marshaler{}
var useNumber bool
// create buffer pool with 16 instances each preallocated with 256 bytes
var bufferPool = bpool.NewSizedBufferPool(16, 256)
var (
defaultGRPCCodecs = map[string]encoding.Codec{
"application/json": jsonCodec{},
"application/proto": protoCodec{},
"application/protobuf": protoCodec{},
"application/octet-stream": protoCodec{},
"application/grpc": protoCodec{},
"application/grpc+json": jsonCodec{},
"application/grpc+proto": protoCodec{},
"application/grpc+bytes": bytesCodec{},
}
)
// UseNumber fix unmarshal Number(8234567890123456789) to interface(8.234567890123457e+18)
func UseNumber() {
useNumber = true
}
func (w wrapCodec) String() string {
return w.Codec.Name()
}
func (w wrapCodec) Marshal(v interface{}) ([]byte, error) {
b, ok := v.(*bytes.Frame)
if ok {
return b.Data, nil
}
return w.Codec.Marshal(v)
}
func (w wrapCodec) Unmarshal(data []byte, v interface{}) error {
b, ok := v.(*bytes.Frame)
if ok {
b.Data = data
return nil
}
return w.Codec.Unmarshal(data, v)
}
func (protoCodec) Marshal(v interface{}) ([]byte, error) {
switch m := v.(type) {
case *bytes.Frame:
return m.Data, nil
case proto.Message:
return proto.Marshal(m)
}
return nil, fmt.Errorf("failed to marshal: %v is not type of *bytes.Frame or proto.Message", v)
}
func (protoCodec) Unmarshal(data []byte, v interface{}) error {
m, ok := v.(proto.Message)
if !ok {
return fmt.Errorf("failed to unmarshal: %v is not type of proto.Message", v)
}
return proto.Unmarshal(data, m)
}
func (protoCodec) Name() string {
return "proto"
}
func (bytesCodec) Marshal(v interface{}) ([]byte, error) {
b, ok := v.(*[]byte)
if !ok {
return nil, fmt.Errorf("failed to marshal: %v is not type of *[]byte", v)
}
return *b, nil
}
func (bytesCodec) Unmarshal(data []byte, v interface{}) error {
b, ok := v.(*[]byte)
if !ok {
return fmt.Errorf("failed to unmarshal: %v is not type of *[]byte", v)
}
*b = data
return nil
}
func (bytesCodec) Name() string {
return "bytes"
}
func (jsonCodec) Marshal(v interface{}) ([]byte, error) {
if b, ok := v.(*bytes.Frame); ok {
return b.Data, nil
}
if pb, ok := v.(proto.Message); ok {
buf := bufferPool.Get()
defer bufferPool.Put(buf)
if err := jsonpbMarshaler.Marshal(buf, pb); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
return json.Marshal(v)
}
func (jsonCodec) Unmarshal(data []byte, v interface{}) error {
if len(data) == 0 {
return nil
}
if b, ok := v.(*bytes.Frame); ok {
b.Data = data
return nil
}
if pb, ok := v.(proto.Message); ok {
return jsonpb.Unmarshal(b.NewReader(data), pb)
}
dec := json.NewDecoder(b.NewReader(data))
if useNumber {
dec.UseNumber()
}
return dec.Decode(v)
}
func (jsonCodec) Name() string {
return "json"
}
type grpcCodec struct {
// headers
id string
target string
method string
endpoint string
s grpc.ClientStream
c encoding.Codec
}
func (g *grpcCodec) ReadHeader(m *codec.Message, mt codec.MessageType) error {
md, err := g.s.Header()
if err != nil {
return err
}
if m == nil {
m = new(codec.Message)
}
if m.Header == nil {
m.Header = make(map[string]string, len(md))
}
for k, v := range md {
m.Header[k] = strings.Join(v, ",")
}
m.Id = g.id
m.Target = g.target
m.Method = g.method
m.Endpoint = g.endpoint
return nil
}
func (g *grpcCodec) ReadBody(v interface{}) error {
if f, ok := v.(*bytes.Frame); ok {
return g.s.RecvMsg(f)
}
return g.s.RecvMsg(v)
}
func (g *grpcCodec) Write(m *codec.Message, v interface{}) error {
// if we don't have a body
if v != nil {
return g.s.SendMsg(v)
}
// write the body using the framing codec
return g.s.SendMsg(&bytes.Frame{Data: m.Body})
}
func (g *grpcCodec) Close() error {
return g.s.CloseSend()
}
func (g *grpcCodec) String() string {
return g.c.Name()
}

View File

@@ -1,39 +0,0 @@
package grpc
import (
"github.com/micro/go-micro/v3/errors"
"google.golang.org/grpc/status"
)
func microError(err error) error {
// no error
switch err {
case nil:
return nil
}
if verr, ok := err.(*errors.Error); ok {
return verr
}
// grpc error
s, ok := status.FromError(err)
if !ok {
return err
}
// return first error from details
if details := s.Details(); len(details) > 0 {
if verr, ok := details[0].(error); ok {
return microError(verr)
}
}
// try to decode micro *errors.Error
if e := errors.Parse(s.Message()); e.Code > 0 {
return e // actually a micro error
}
// fallback
return errors.InternalServerError("go.micro.client", s.Message())
}

View File

@@ -1,729 +0,0 @@
// Package grpc provides a gRPC client
package grpc
import (
"context"
"crypto/tls"
"fmt"
"net"
"reflect"
"strings"
"sync/atomic"
"time"
"github.com/micro/go-micro/v3/broker"
"github.com/micro/go-micro/v3/client"
raw "github.com/micro/go-micro/v3/codec/bytes"
"github.com/micro/go-micro/v3/errors"
"github.com/micro/go-micro/v3/metadata"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/encoding"
gmetadata "google.golang.org/grpc/metadata"
)
type grpcClient struct {
opts client.Options
pool *pool
once atomic.Value
}
func init() {
encoding.RegisterCodec(wrapCodec{jsonCodec{}})
encoding.RegisterCodec(wrapCodec{protoCodec{}})
encoding.RegisterCodec(wrapCodec{bytesCodec{}})
}
// secure returns the dial option for whether its a secure or insecure connection
func (g *grpcClient) secure(addr string) grpc.DialOption {
// first we check if theres'a tls config
if g.opts.Context != nil {
if v := g.opts.Context.Value(tlsAuth{}); v != nil {
tls := v.(*tls.Config)
creds := credentials.NewTLS(tls)
// return tls config if it exists
return grpc.WithTransportCredentials(creds)
}
}
// default config
tlsConfig := &tls.Config{}
defaultCreds := grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))
// check if the address is prepended with https
if strings.HasPrefix(addr, "https://") {
return defaultCreds
}
// if no port is specified or port is 443 default to tls
_, port, err := net.SplitHostPort(addr)
// assuming with no port its going to be secured
if port == "443" {
return defaultCreds
} else if err != nil && strings.Contains(err.Error(), "missing port in address") {
return defaultCreds
}
// other fallback to insecure
return grpc.WithInsecure()
}
func (g *grpcClient) call(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions) error {
var header map[string]string
header = make(map[string]string)
if md, ok := metadata.FromContext(ctx); ok {
header = make(map[string]string, len(md))
for k, v := range md {
header[strings.ToLower(k)] = v
}
} else {
header = make(map[string]string)
}
// set timeout in nanoseconds
header["timeout"] = fmt.Sprintf("%d", opts.RequestTimeout)
// set the content type for the request
header["x-content-type"] = req.ContentType()
md := gmetadata.New(header)
ctx = gmetadata.NewOutgoingContext(ctx, md)
cf, err := g.newGRPCCodec(req.ContentType())
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
maxRecvMsgSize := g.maxRecvMsgSizeValue()
maxSendMsgSize := g.maxSendMsgSizeValue()
var grr error
grpcDialOptions := []grpc.DialOption{
grpc.WithTimeout(opts.DialTimeout),
g.secure(addr),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(maxRecvMsgSize),
grpc.MaxCallSendMsgSize(maxSendMsgSize),
),
}
if opts := g.getGrpcDialOptions(); opts != nil {
grpcDialOptions = append(grpcDialOptions, opts...)
}
cc, err := g.pool.getConn(addr, grpcDialOptions...)
if err != nil {
return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error sending request: %v", err))
}
defer func() {
// defer execution of release
g.pool.release(addr, cc, grr)
}()
ch := make(chan error, 1)
go func() {
grpcCallOptions := []grpc.CallOption{
grpc.ForceCodec(cf),
grpc.CallContentSubtype(cf.Name())}
if opts := g.getGrpcCallOptions(); opts != nil {
grpcCallOptions = append(grpcCallOptions, opts...)
}
err := cc.Invoke(ctx, methodToGRPC(req.Service(), req.Endpoint()), req.Body(), rsp, grpcCallOptions...)
ch <- microError(err)
}()
select {
case err := <-ch:
grr = err
case <-ctx.Done():
grr = errors.Timeout("go.micro.client", "%v", ctx.Err())
}
return grr
}
func (g *grpcClient) stream(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions) error {
var header map[string]string
if md, ok := metadata.FromContext(ctx); ok {
header = make(map[string]string, len(md))
for k, v := range md {
header[k] = v
}
} else {
header = make(map[string]string)
}
// set timeout in nanoseconds
if opts.StreamTimeout > time.Duration(0) {
header["timeout"] = fmt.Sprintf("%d", opts.StreamTimeout)
}
// set the content type for the request
header["x-content-type"] = req.ContentType()
md := gmetadata.New(header)
ctx = gmetadata.NewOutgoingContext(ctx, md)
cf, err := g.newGRPCCodec(req.ContentType())
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
var dialCtx context.Context
var cancel context.CancelFunc
if opts.DialTimeout >= 0 {
dialCtx, cancel = context.WithTimeout(ctx, opts.DialTimeout)
} else {
dialCtx, cancel = context.WithCancel(ctx)
}
defer cancel()
wc := wrapCodec{cf}
grpcDialOptions := []grpc.DialOption{
grpc.WithTimeout(opts.DialTimeout),
g.secure(addr),
}
if opts := g.getGrpcDialOptions(); opts != nil {
grpcDialOptions = append(grpcDialOptions, opts...)
}
cc, err := grpc.DialContext(dialCtx, addr, grpcDialOptions...)
if err != nil {
return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error sending request: %v", err))
}
desc := &grpc.StreamDesc{
StreamName: req.Service() + req.Endpoint(),
ClientStreams: true,
ServerStreams: true,
}
grpcCallOptions := []grpc.CallOption{
grpc.ForceCodec(wc),
grpc.CallContentSubtype(cf.Name()),
}
if opts := g.getGrpcCallOptions(); opts != nil {
grpcCallOptions = append(grpcCallOptions, opts...)
}
// create a new cancelling context
newCtx, cancel := context.WithCancel(ctx)
st, err := cc.NewStream(newCtx, desc, methodToGRPC(req.Service(), req.Endpoint()), grpcCallOptions...)
if err != nil {
// we need to cleanup as we dialled and created a context
// cancel the context
cancel()
// close the connection
cc.Close()
// now return the error
return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error creating stream: %v", err))
}
codec := &grpcCodec{
s: st,
c: wc,
}
// set request codec
if r, ok := req.(*grpcRequest); ok {
r.codec = codec
}
// setup the stream response
stream := &grpcStream{
ClientStream: st,
context: ctx,
request: req,
response: &response{
conn: cc,
stream: st,
codec: cf,
gcodec: codec,
},
conn: cc,
cancel: cancel,
}
// set the stream as the response
val := reflect.ValueOf(rsp).Elem()
val.Set(reflect.ValueOf(stream).Elem())
return nil
}
func (g *grpcClient) poolMaxStreams() int {
if g.opts.Context == nil {
return DefaultPoolMaxStreams
}
v := g.opts.Context.Value(poolMaxStreams{})
if v == nil {
return DefaultPoolMaxStreams
}
return v.(int)
}
func (g *grpcClient) poolMaxIdle() int {
if g.opts.Context == nil {
return DefaultPoolMaxIdle
}
v := g.opts.Context.Value(poolMaxIdle{})
if v == nil {
return DefaultPoolMaxIdle
}
return v.(int)
}
func (g *grpcClient) maxRecvMsgSizeValue() int {
if g.opts.Context == nil {
return DefaultMaxRecvMsgSize
}
v := g.opts.Context.Value(maxRecvMsgSizeKey{})
if v == nil {
return DefaultMaxRecvMsgSize
}
return v.(int)
}
func (g *grpcClient) maxSendMsgSizeValue() int {
if g.opts.Context == nil {
return DefaultMaxSendMsgSize
}
v := g.opts.Context.Value(maxSendMsgSizeKey{})
if v == nil {
return DefaultMaxSendMsgSize
}
return v.(int)
}
func (g *grpcClient) newGRPCCodec(contentType string) (encoding.Codec, error) {
codecs := make(map[string]encoding.Codec)
if g.opts.Context != nil {
if v := g.opts.Context.Value(codecsKey{}); v != nil {
codecs = v.(map[string]encoding.Codec)
}
}
if c, ok := codecs[contentType]; ok {
return wrapCodec{c}, nil
}
if c, ok := defaultGRPCCodecs[contentType]; ok {
return wrapCodec{c}, nil
}
return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType)
}
func (g *grpcClient) Init(opts ...client.Option) error {
size := g.opts.PoolSize
ttl := g.opts.PoolTTL
for _, o := range opts {
o(&g.opts)
}
// update pool configuration if the options changed
if size != g.opts.PoolSize || ttl != g.opts.PoolTTL {
g.pool.Lock()
g.pool.size = g.opts.PoolSize
g.pool.ttl = int64(g.opts.PoolTTL.Seconds())
g.pool.Unlock()
}
return nil
}
func (g *grpcClient) Options() client.Options {
return g.opts
}
func (g *grpcClient) NewMessage(topic string, msg interface{}, opts ...client.MessageOption) client.Message {
return newGRPCEvent(topic, msg, g.opts.ContentType, opts...)
}
func (g *grpcClient) NewRequest(service, method string, req interface{}, reqOpts ...client.RequestOption) client.Request {
return newGRPCRequest(service, method, req, g.opts.ContentType, reqOpts...)
}
func (g *grpcClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
if req == nil {
return errors.InternalServerError("go.micro.client", "req is nil")
} else if rsp == nil {
return errors.InternalServerError("go.micro.client", "rsp is nil")
}
// make a copy of call opts
callOpts := g.opts.CallOptions
for _, opt := range opts {
opt(&callOpts)
}
// check if we already have a deadline
d, ok := ctx.Deadline()
if !ok {
// no deadline so we create a new one
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, callOpts.RequestTimeout)
defer cancel()
} else {
// got a deadline so no need to setup context
// but we need to set the timeout we pass along
opt := client.WithRequestTimeout(time.Until(d))
opt(&callOpts)
}
// should we noop right here?
select {
case <-ctx.Done():
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
default:
}
// make copy of call method
gcall := g.call
// wrap the call in reverse
for i := len(callOpts.CallWrappers); i > 0; i-- {
gcall = callOpts.CallWrappers[i-1](gcall)
}
// use the router passed as a call option, or fallback to the rpc clients router
if callOpts.Router == nil {
callOpts.Router = g.opts.Router
}
if callOpts.Selector == nil {
callOpts.Selector = g.opts.Selector
}
// inject proxy address
// TODO: don't even bother using Lookup/Select in this case
if len(g.opts.Proxy) > 0 {
callOpts.Address = []string{g.opts.Proxy}
}
// lookup the route to send the reques to
// TODO apply any filtering here
routes, err := g.opts.Lookup(ctx, req, callOpts)
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
// balance the list of nodes
next, err := callOpts.Selector.Select(routes)
if err != nil {
return err
}
// return errors.New("go.micro.client", "request timeout", 408)
call := func(i int) error {
// call backoff first. Someone may want an initial start delay
t, err := callOpts.Backoff(ctx, req, i)
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
// only sleep if greater than 0
if t.Seconds() > 0 {
time.Sleep(t)
}
// get the next node
node := next()
// make the call
err = gcall(ctx, node, req, rsp, callOpts)
// record the result of the call to inform future routing decisions
g.opts.Selector.Record(node, err)
// try and transform the error to a go-micro error
if verr, ok := err.(*errors.Error); ok {
return verr
}
return err
}
ch := make(chan error, callOpts.Retries+1)
var gerr error
for i := 0; i <= callOpts.Retries; i++ {
go func(i int) {
ch <- call(i)
}(i)
select {
case <-ctx.Done():
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
case err := <-ch:
// if the call succeeded lets bail early
if err == nil {
return nil
}
retry, rerr := callOpts.Retry(ctx, req, i, err)
if rerr != nil {
return rerr
}
if !retry {
return err
}
gerr = err
}
}
return gerr
}
func (g *grpcClient) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
// make a copy of call opts
callOpts := g.opts.CallOptions
for _, opt := range opts {
opt(&callOpts)
}
// #200 - streams shouldn't have a request timeout set on the context
// should we noop right here?
select {
case <-ctx.Done():
return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
default:
}
// make a copy of stream
gstream := g.stream
// wrap the call in reverse
for i := len(callOpts.CallWrappers); i > 0; i-- {
gstream = callOpts.CallWrappers[i-1](gstream)
}
// use the router passed as a call option, or fallback to the rpc clients router
if callOpts.Router == nil {
callOpts.Router = g.opts.Router
}
if callOpts.Selector == nil {
callOpts.Selector = g.opts.Selector
}
// inject proxy address
// TODO: don't even bother using Lookup/Select in this case
if len(g.opts.Proxy) > 0 {
callOpts.Address = []string{g.opts.Proxy}
}
// lookup the route to send the reques to
// TODO: move to internal lookup func
routes, err := g.opts.Lookup(ctx, req, callOpts)
if err != nil {
return nil, errors.InternalServerError("go.micro.client", err.Error())
}
// balance the list of nodes
next, err := callOpts.Selector.Select(routes)
if err != nil {
return nil, err
}
call := func(i int) (client.Stream, error) {
// call backoff first. Someone may want an initial start delay
t, err := callOpts.Backoff(ctx, req, i)
if err != nil {
return nil, errors.InternalServerError("go.micro.client", err.Error())
}
// only sleep if greater than 0
if t.Seconds() > 0 {
time.Sleep(t)
}
// get the next node
node := next()
// make the call
stream := &grpcStream{}
err = g.stream(ctx, node, req, stream, callOpts)
// record the result of the call to inform future routing decisions
g.opts.Selector.Record(node, err)
// try and transform the error to a go-micro error
if verr, ok := err.(*errors.Error); ok {
return nil, verr
}
g.opts.Selector.Record(node, err)
return stream, err
}
type response struct {
stream client.Stream
err error
}
ch := make(chan response, callOpts.Retries+1)
var grr error
for i := 0; i <= callOpts.Retries; i++ {
go func(i int) {
s, err := call(i)
ch <- response{s, err}
}(i)
select {
case <-ctx.Done():
return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
case rsp := <-ch:
// if the call succeeded lets bail early
if rsp.err == nil {
return rsp.stream, nil
}
retry, rerr := callOpts.Retry(ctx, req, i, grr)
if rerr != nil {
return nil, rerr
}
if !retry {
return nil, rsp.err
}
grr = rsp.err
}
}
return nil, grr
}
func (g *grpcClient) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error {
var options client.PublishOptions
var body []byte
// fail early on connect error
if !g.once.Load().(bool) {
if err := g.opts.Broker.Connect(); err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
g.once.Store(true)
}
for _, o := range opts {
o(&options)
}
md, ok := metadata.FromContext(ctx)
if !ok {
md = make(map[string]string)
}
md["Content-Type"] = p.ContentType()
md["Micro-Topic"] = p.Topic()
// passed in raw data
if d, ok := p.Payload().(*raw.Frame); ok {
body = d.Data
} else {
// use codec for payload
cf, err := g.newGRPCCodec(p.ContentType())
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
// set the body
b, err := cf.Marshal(p.Payload())
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
body = b
}
topic := p.Topic()
// get the exchange
if len(options.Exchange) > 0 {
topic = options.Exchange
}
return g.opts.Broker.Publish(topic, &broker.Message{
Header: md,
Body: body,
}, broker.PublishContext(options.Context))
}
func (g *grpcClient) String() string {
return "grpc"
}
func (g *grpcClient) getGrpcDialOptions() []grpc.DialOption {
if g.opts.CallOptions.Context == nil {
return nil
}
v := g.opts.CallOptions.Context.Value(grpcDialOptions{})
if v == nil {
return nil
}
opts, ok := v.([]grpc.DialOption)
if !ok {
return nil
}
return opts
}
func (g *grpcClient) getGrpcCallOptions() []grpc.CallOption {
if g.opts.CallOptions.Context == nil {
return nil
}
v := g.opts.CallOptions.Context.Value(grpcCallOptions{})
if v == nil {
return nil
}
opts, ok := v.([]grpc.CallOption)
if !ok {
return nil
}
return opts
}
func newClient(opts ...client.Option) client.Client {
options := client.NewOptions()
// default content type for grpc
options.ContentType = "application/grpc+proto"
for _, o := range opts {
o(&options)
}
rc := &grpcClient{
opts: options,
}
rc.once.Store(false)
rc.pool = newPool(options.PoolSize, options.PoolTTL, rc.poolMaxIdle(), rc.poolMaxStreams())
c := client.Client(rc)
// wrap in reverse
for i := len(options.Wrappers); i > 0; i-- {
c = options.Wrappers[i-1](c)
}
return c
}
func NewClient(opts ...client.Option) client.Client {
return newClient(opts...)
}

View File

@@ -1,218 +0,0 @@
package grpc
import (
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
)
type pool struct {
size int
ttl int64
// max streams on a *poolConn
maxStreams int
// max idle conns
maxIdle int
sync.Mutex
conns map[string]*streamsPool
}
type streamsPool struct {
// head of list
head *poolConn
// busy conns list
busy *poolConn
// the siza of list
count int
// idle conn
idle int
}
type poolConn struct {
// grpc conn
*grpc.ClientConn
err error
addr string
// pool and streams pool
pool *pool
sp *streamsPool
streams int
created int64
// list
pre *poolConn
next *poolConn
in bool
}
func newPool(size int, ttl time.Duration, idle int, ms int) *pool {
if ms <= 0 {
ms = 1
}
if idle < 0 {
idle = 0
}
return &pool{
size: size,
ttl: int64(ttl.Seconds()),
maxStreams: ms,
maxIdle: idle,
conns: make(map[string]*streamsPool),
}
}
func (p *pool) getConn(addr string, opts ...grpc.DialOption) (*poolConn, error) {
now := time.Now().Unix()
p.Lock()
sp, ok := p.conns[addr]
if !ok {
sp = &streamsPool{head: &poolConn{}, busy: &poolConn{}, count: 0, idle: 0}
p.conns[addr] = sp
}
// while we have conns check streams and then return one
// otherwise we'll create a new conn
conn := sp.head.next
for conn != nil {
// check conn state
// https://github.com/grpc/grpc/blob/master/doc/connectivity-semantics-and-api.md
switch conn.GetState() {
case connectivity.Connecting:
conn = conn.next
continue
case connectivity.Shutdown:
next := conn.next
if conn.streams == 0 {
removeConn(conn)
sp.idle--
}
conn = next
continue
case connectivity.TransientFailure:
next := conn.next
if conn.streams == 0 {
removeConn(conn)
conn.ClientConn.Close()
sp.idle--
}
conn = next
continue
case connectivity.Ready:
case connectivity.Idle:
}
// a old conn
if now-conn.created > p.ttl {
next := conn.next
if conn.streams == 0 {
removeConn(conn)
conn.ClientConn.Close()
sp.idle--
}
conn = next
continue
}
// a busy conn
if conn.streams >= p.maxStreams {
next := conn.next
removeConn(conn)
addConnAfter(conn, sp.busy)
conn = next
continue
}
// a idle conn
if conn.streams == 0 {
sp.idle--
}
// a good conn
conn.streams++
p.Unlock()
return conn, nil
}
p.Unlock()
// create new conn
cc, err := grpc.Dial(addr, opts...)
if err != nil {
return nil, err
}
conn = &poolConn{cc, nil, addr, p, sp, 1, time.Now().Unix(), nil, nil, false}
// add conn to streams pool
p.Lock()
if sp.count < p.size {
addConnAfter(conn, sp.head)
}
p.Unlock()
return conn, nil
}
func (p *pool) release(addr string, conn *poolConn, err error) {
p.Lock()
p, sp, created := conn.pool, conn.sp, conn.created
// try to add conn
if !conn.in && sp.count < p.size {
addConnAfter(conn, sp.head)
}
if !conn.in {
p.Unlock()
conn.ClientConn.Close()
return
}
// a busy conn
if conn.streams >= p.maxStreams {
removeConn(conn)
addConnAfter(conn, sp.head)
}
conn.streams--
// if streams == 0, we can do something
if conn.streams == 0 {
// 1. it has errored
// 2. too many idle conn or
// 3. conn is too old
now := time.Now().Unix()
if err != nil || sp.idle >= p.maxIdle || now-created > p.ttl {
removeConn(conn)
p.Unlock()
conn.ClientConn.Close()
return
}
sp.idle++
}
p.Unlock()
return
}
func (conn *poolConn) Close() {
conn.pool.release(conn.addr, conn, conn.err)
}
func removeConn(conn *poolConn) {
if conn.pre != nil {
conn.pre.next = conn.next
}
if conn.next != nil {
conn.next.pre = conn.pre
}
conn.pre = nil
conn.next = nil
conn.in = false
conn.sp.count--
return
}
func addConnAfter(conn *poolConn, after *poolConn) {
conn.next = after.next
conn.pre = after
if after.next != nil {
after.next.pre = conn
}
after.next = conn
conn.in = true
conn.sp.count++
return
}

View File

@@ -1,64 +0,0 @@
package grpc
import (
"context"
"net"
"testing"
"time"
"google.golang.org/grpc"
pgrpc "google.golang.org/grpc"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
)
func testPool(t *testing.T, size int, ttl time.Duration, idle int, ms int) {
// setup server
l, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
defer l.Close()
s := pgrpc.NewServer()
pb.RegisterGreeterServer(s, &greeterServer{})
go s.Serve(l)
defer s.Stop()
// zero pool
p := newPool(size, ttl, idle, ms)
for i := 0; i < 10; i++ {
// get a conn
cc, err := p.getConn(l.Addr().String(), grpc.WithInsecure())
if err != nil {
t.Fatal(err)
}
rsp := pb.HelloReply{}
err = cc.Invoke(context.TODO(), "/helloworld.Greeter/SayHello", &pb.HelloRequest{Name: "John"}, &rsp)
if err != nil {
t.Fatal(err)
}
if rsp.Message != "Hello John" {
t.Fatalf("Got unexpected response %v", rsp.Message)
}
// release the conn
p.release(l.Addr().String(), cc, nil)
p.Lock()
if i := p.conns[l.Addr().String()].count; i > size {
p.Unlock()
t.Fatalf("pool size %d is greater than expected %d", i, size)
}
p.Unlock()
}
}
func TestGRPCPool(t *testing.T) {
testPool(t, 0, time.Minute, 10, 2)
testPool(t, 2, time.Minute, 10, 1)
}

View File

@@ -1,108 +0,0 @@
package grpc
import (
"context"
"net"
"testing"
"github.com/micro/go-micro/v3/client"
"github.com/micro/go-micro/v3/errors"
"github.com/micro/go-micro/v3/registry"
"github.com/micro/go-micro/v3/registry/memory"
"github.com/micro/go-micro/v3/router"
regRouter "github.com/micro/go-micro/v3/router/registry"
pgrpc "google.golang.org/grpc"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
)
// server is used to implement helloworld.GreeterServer.
type greeterServer struct{}
// SayHello implements helloworld.GreeterServer
func (g *greeterServer) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
if in.Name == "Error" {
return nil, &errors.Error{Id: "1", Code: 99, Detail: "detail"}
}
return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
func TestGRPCClient(t *testing.T) {
l, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
defer l.Close()
s := pgrpc.NewServer()
pb.RegisterGreeterServer(s, &greeterServer{})
go s.Serve(l)
defer s.Stop()
// create mock registry
r := memory.NewRegistry()
// register service
r.Register(&registry.Service{
Name: "helloworld",
Version: "test",
Nodes: []*registry.Node{
{
Id: "test-1",
Address: l.Addr().String(),
Metadata: map[string]string{
"protocol": "grpc",
},
},
},
})
// create router
rtr := regRouter.NewRouter(router.Registry(r))
// create client
c := NewClient(client.Router(rtr))
testMethods := []string{
"/helloworld.Greeter/SayHello",
"Greeter.SayHello",
}
for _, method := range testMethods {
req := c.NewRequest("helloworld", method, &pb.HelloRequest{
Name: "John",
})
rsp := pb.HelloReply{}
err = c.Call(context.TODO(), req, &rsp)
if err != nil {
t.Fatal(err)
}
if rsp.Message != "Hello John" {
t.Fatalf("Got unexpected response %v", rsp.Message)
}
}
req := c.NewRequest("helloworld", "/helloworld.Greeter/SayHello", &pb.HelloRequest{
Name: "Error",
})
rsp := pb.HelloReply{}
err = c.Call(context.TODO(), req, &rsp)
if err == nil {
t.Fatal("nil error received")
}
verr, ok := err.(*errors.Error)
if !ok {
t.Fatalf("invalid error received %#+v\n", err)
}
if verr.Code != 99 && verr.Id != "1" && verr.Detail != "detail" {
t.Fatalf("invalid error received %#+v\n", verr)
}
}

Some files were not shown because too many files have changed in this diff Show More