Initial QSfera import
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# Develop tools
|
||||
/.vscode/
|
||||
/.idea/
|
||||
/.trunk
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
_build
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# vim temp files
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# go work files
|
||||
go.work
|
||||
go.work.sum
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
# This file contains all available configuration options
|
||||
# with their default values.
|
||||
|
||||
# options for analysis running
|
||||
run:
|
||||
# go: '1.18'
|
||||
# default concurrency is a available CPU number
|
||||
# concurrency: 4
|
||||
|
||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||
deadline: 10m
|
||||
|
||||
# exit code when at least one issue was found, default is 1
|
||||
issues-exit-code: 1
|
||||
|
||||
# include test files or not, default is true
|
||||
tests: true
|
||||
|
||||
# which files to skip: they will be analyzed, but issues from them
|
||||
# won't be reported. Default value is empty list, but there is
|
||||
# no need to include all autogenerated files, we confidently recognize
|
||||
# autogenerated files. If it's not please let us know.
|
||||
skip-files:
|
||||
[]
|
||||
# - .*\\.pb\\.go$
|
||||
|
||||
allow-parallel-runners: true
|
||||
|
||||
# list of build tags, all linters use it. Default is empty list.
|
||||
build-tags: []
|
||||
|
||||
# output configuration options
|
||||
output:
|
||||
# Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
|
||||
#
|
||||
# Multiple can be specified by separating them by comma, output can be provided
|
||||
# for each of them by separating format name and path by colon symbol.
|
||||
# Output path can be either `stdout`, `stderr` or path to the file to write to.
|
||||
# Example: "checkstyle:report.json,colored-line-number"
|
||||
#
|
||||
# Default: colored-line-number
|
||||
format: colored-line-number
|
||||
# Print lines of code with issue.
|
||||
# Default: true
|
||||
print-issued-lines: true
|
||||
# Print linter name in the end of issue text.
|
||||
# Default: true
|
||||
print-linter-name: true
|
||||
# Make issues output unique by line.
|
||||
# Default: true
|
||||
uniq-by-line: true
|
||||
# Add a prefix to the output file references.
|
||||
# Default is no prefix.
|
||||
path-prefix: ""
|
||||
# Sort results by: filepath, line and column.
|
||||
sort-results: true
|
||||
|
||||
# all available settings of specific linters
|
||||
linters-settings:
|
||||
wsl:
|
||||
allow-cuddle-with-calls: ["Lock", "RLock", "defer"]
|
||||
funlen:
|
||||
lines: 80
|
||||
statements: 60
|
||||
varnamelen:
|
||||
# The longest distance, in source lines, that is being considered a "small scope".
|
||||
# Variables used in at most this many lines will be ignored.
|
||||
# Default: 5
|
||||
max-distance: 26
|
||||
ignore-names:
|
||||
- err
|
||||
- id
|
||||
- ch
|
||||
- wg
|
||||
- mu
|
||||
ignore-decls:
|
||||
- c echo.Context
|
||||
- t testing.T
|
||||
- f *foo.Bar
|
||||
- e error
|
||||
- i int
|
||||
- const C
|
||||
- T any
|
||||
- m map[string]int
|
||||
errcheck:
|
||||
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
|
||||
# default is false: such cases aren't reported by default.
|
||||
check-type-assertions: true
|
||||
|
||||
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
|
||||
# default is false: such cases aren't reported by default.
|
||||
check-blank: true
|
||||
govet:
|
||||
# report about shadowed variables
|
||||
check-shadowing: false
|
||||
gofmt:
|
||||
# simplify code: gofmt with `-s` option, true by default
|
||||
simplify: true
|
||||
gocyclo:
|
||||
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
||||
min-complexity: 15
|
||||
maligned:
|
||||
# print struct with more effective memory layout or not, false by default
|
||||
suggest-new: true
|
||||
dupl:
|
||||
# tokens count to trigger issue, 150 by default
|
||||
threshold: 100
|
||||
goconst:
|
||||
# minimal length of string constant, 3 by default
|
||||
min-len: 3
|
||||
# minimal occurrences count to trigger, 3 by default
|
||||
min-occurrences: 3
|
||||
depguard:
|
||||
list-type: blacklist
|
||||
# Packages listed here will reported as error if imported
|
||||
packages:
|
||||
- github.com/golang/protobuf/proto
|
||||
misspell:
|
||||
# Correct spellings using locale preferences for US or UK.
|
||||
# Default is to use a neutral variety of English.
|
||||
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
|
||||
locale: US
|
||||
lll:
|
||||
# max line length, lines longer will be reported. Default is 120.
|
||||
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
|
||||
line-length: 120
|
||||
# tab width in spaces. Default to 1.
|
||||
tab-width: 1
|
||||
unused:
|
||||
# treat code as a program (not a library) and report unused exported identifiers; default is false.
|
||||
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
|
||||
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
|
||||
# with golangci-lint call it on a directory with the changed file.
|
||||
check-exported: false
|
||||
unparam:
|
||||
# call graph construction algorithm (cha, rta). In general, use cha for libraries,
|
||||
# and rta for programs with main packages. Default is cha.
|
||||
algo: cha
|
||||
|
||||
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
|
||||
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
|
||||
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
|
||||
# with golangci-lint call it on a directory with the changed file.
|
||||
check-exported: false
|
||||
nakedret:
|
||||
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
|
||||
max-func-lines: 60
|
||||
nolintlint:
|
||||
allow-unused: false
|
||||
allow-leading-space: false
|
||||
allow-no-explanation: []
|
||||
require-explanation: false
|
||||
require-specific: true
|
||||
prealloc:
|
||||
# XXX: we don't recommend using this linter before doing performance profiling.
|
||||
# For most programs usage of prealloc will be a premature optimization.
|
||||
|
||||
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
|
||||
# True by default.
|
||||
simple: true
|
||||
range-loops: true # Report preallocation suggestions on range loops, true by default
|
||||
for-loops: false # Report preallocation suggestions on for loops, false by default
|
||||
cyclop:
|
||||
# the maximal code complexity to report
|
||||
max-complexity: 20
|
||||
gomoddirectives:
|
||||
replace-local: true
|
||||
retract-allow-no-explanation: false
|
||||
exclude-forbidden: true
|
||||
|
||||
linters:
|
||||
enable-all: true
|
||||
disable-all: false
|
||||
fast: false
|
||||
disable:
|
||||
- golint
|
||||
- varcheck
|
||||
- ifshort
|
||||
- structcheck
|
||||
- deadcode
|
||||
# - nosnakecase
|
||||
- interfacer
|
||||
- maligned
|
||||
- scopelint
|
||||
- exhaustivestruct
|
||||
- testpackage
|
||||
- promlinter
|
||||
- nonamedreturns
|
||||
- makezero
|
||||
- gofumpt
|
||||
- nlreturn
|
||||
- thelper
|
||||
|
||||
# Can be considered to be enabled
|
||||
- gochecknoinits
|
||||
- gochecknoglobals # RIP
|
||||
- dogsled
|
||||
- wrapcheck
|
||||
- paralleltest
|
||||
- ireturn
|
||||
- gomnd
|
||||
- goerr113
|
||||
- exhaustruct
|
||||
- containedctx
|
||||
- godox
|
||||
- forcetypeassert
|
||||
- gci
|
||||
- lll
|
||||
|
||||
issues:
|
||||
# List of regexps of issue texts to exclude, empty list by default.
|
||||
# But independently from this option we use default exclude patterns,
|
||||
# it can be disabled by `exclude-use-default: false`. To list all
|
||||
# excluded by default patterns execute `golangci-lint run --help`
|
||||
# exclude:
|
||||
# - package comment should be of the form "Package services ..." # revive
|
||||
# - ^ST1000 # ST1000: at least one file in a package should have a package comment (stylecheck)
|
||||
|
||||
# exclude-rules:
|
||||
# - path: internal/app/machined/pkg/system/services
|
||||
# linters:
|
||||
# - dupl
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- dupl
|
||||
- gosec
|
||||
- funlen
|
||||
- varnamelen
|
||||
- wsl
|
||||
|
||||
# Independently from option `exclude` we use default exclude patterns,
|
||||
# it can be disabled by this option. To list all
|
||||
# excluded by default patterns execute `golangci-lint run --help`.
|
||||
# Default value for this option is true.
|
||||
exclude-use-default: false
|
||||
|
||||
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
||||
max-issues-per-linter: 0
|
||||
|
||||
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
||||
max-same-issues: 0
|
||||
|
||||
# Show only new issues: if there are unstaged changes or untracked files,
|
||||
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
|
||||
# It's a super-useful option for integration of golangci-lint into existing
|
||||
# large codebase. It's not practical to fix all existing issues at the moment
|
||||
# of integration: much better don't allow issues in new code.
|
||||
# Default is false.
|
||||
new: false
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
labelType: long
|
||||
coverThreshold: 70
|
||||
buildStyle:
|
||||
bold: true
|
||||
foreground: yellow
|
||||
startStyle:
|
||||
foreground: lightBlack
|
||||
passStyle:
|
||||
foreground: green
|
||||
failStyle:
|
||||
bold: true
|
||||
foreground: "#821515"
|
||||
skipStyle:
|
||||
foreground: lightBlack
|
||||
passPackageStyle:
|
||||
foreground: green
|
||||
hide: false
|
||||
failPackageStyle:
|
||||
bold: true
|
||||
foreground: "#821515"
|
||||
coveredStyle:
|
||||
foreground: green
|
||||
uncoveredStyle:
|
||||
bold: true
|
||||
foreground: yellow
|
||||
fileStyle:
|
||||
foreground: cyan
|
||||
lineStyle:
|
||||
foreground: magenta
|
||||
+774
@@ -0,0 +1,774 @@
|
||||
# 4.7.0 (2022/05/19)
|
||||
|
||||
### Features
|
||||
|
||||
- remove unused variable in loop (#2495)
|
||||
- option to disable file watcher (#2485)
|
||||
- Support direct generation of grpc method (#2474)
|
||||
- remove cli
|
||||
- remove dashboard
|
||||
- Remove examples
|
||||
- Add header suppor for Kafka broker plugin; (#2470)
|
||||
- Fix codec/bytes (#2466)
|
||||
- Add Wait option support for sync/etcd plugins (#2459)
|
||||
- #2453 Fix with associated test update (#2454)
|
||||
- style:arrays pre-allocation (#2449)
|
||||
- Bump github.com/nats-io/nats-server/v2 in /plugins/events/natsjs (#2447)
|
||||
- add NATS JetStream events plugin (#2433)
|
||||
- add tls option for sync etcd plugin (#2440)
|
||||
- add new services
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix: consume and publish blocked after rabbitmq reconnecting (#2492)
|
||||
- typo fix; (#2480)
|
||||
- fix https://github.com/asim/go-micro/issues/2344 (#2462)
|
||||
- fix natsjs syntax error, remove TODOs and enable tests (#2446)
|
||||
- http transport deadlock (#2441)
|
||||
- Support for `-micro_out=module=<module_prefix>` for protoc-gen-micro (#2435)
|
||||
|
||||
# 4.6.0 (2022/02/08)
|
||||
|
||||
### Features
|
||||
|
||||
- add service updates (#2418)
|
||||
- Add files via upload
|
||||
- default the content type to json (#2412)
|
||||
- Add Kafka asynchronous send support (#2409)
|
||||
- use read lock and unlock instead of write ones (#2410)
|
||||
- move the api client
|
||||
- Update client_test.go
|
||||
- switch services client
|
||||
- remove the client
|
||||
- add service interfaces
|
||||
- [FEATURE] add changelog (#2400)
|
||||
- nats config plugin (#2397)
|
||||
- Update and rename m3o.go to services.go
|
||||
- Update services/ (#2392)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix client
|
||||
- fix context value nil (#2391)
|
||||
|
||||
# 4.5.0 (2021/12/19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- nats deregister issue (#2384)
|
||||
- fixing f.IsExported undefined issue (#2382)
|
||||
- fix http_transport Recv and Close race condition on buff (#2374)
|
||||
- update protoc-gen-micro install doc
|
||||
- zookeeper registry delete event (#2372)
|
||||
- delete redundant lines (#2364)
|
||||
- modify the dependencies urls (#2363)
|
||||
- ignore unexported field (#2354)
|
||||
- Fix Micro CLI's proto comments (#2353)
|
||||
|
||||
### Features
|
||||
|
||||
- Extend client mock with ability to test publish, and a few useful method like SetResponse and SetSubscriber (#2375)
|
||||
- go micro dashboard (#2361)
|
||||
|
||||
# 4.4.0 (2021/11/11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix(#2333): etcd grpc marshal issue (#2334)
|
||||
|
||||
### Features
|
||||
|
||||
- upgrade to go 1.17 (#2346)
|
||||
- add nats and redis events plugins
|
||||
- add events package (#2341)
|
||||
|
||||
# 4.3.0 (2021/11/01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- flatten cli (#2332)
|
||||
- m3o client changed
|
||||
- use vanity url for cli command
|
||||
- fix broker nsq plugin nil pointer error (#2329)
|
||||
- fix config json slice parsing (#2330)
|
||||
- replace ioutil with io and os (#2327)
|
||||
- fixing #2308 (#2326)
|
||||
- Fix Micro CLI by removing replace line in go.mod
|
||||
- remove unnecessary dependencies between plugins
|
||||
- 1. use default memory registry in grpc plugins (#2317)
|
||||
- Add update rule to Makefile (#2315)
|
||||
- Plugins (#2311)
|
||||
|
||||
### Features
|
||||
|
||||
- Rename gomu to micro (#2325)
|
||||
- stream CloseSend (#2323)
|
||||
- strip protoc-gen-micro go mod
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
- go install go-micro.dev/v4/cmd/micro@v4
|
||||
- go install go-micro.dev/v4/cmd/protoc-gen-micro@v4
|
||||
- upgrade go micro to support stream.CloseSend
|
||||
|
||||
# 4.2.1 (2021/10/14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix gomu.
|
||||
|
||||
# 4.2.0 (2021/10/13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix examples go mod.
|
||||
- update go sums.
|
||||
- move to go-micro.dev.
|
||||
- upgrade protoc-gen-micro to v4.
|
||||
|
||||
# 4.1.0 (2021/10/12)
|
||||
|
||||
- v4.1.0.
|
||||
|
||||
# 4.0.0 (2021/10/12)
|
||||
|
||||
### Features
|
||||
|
||||
- Vanity URL go-micro.dev([#2271](https://github.com/asim/go-micro/issues/2271)).
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
- upgrade github.com/asim/go-micro/v3 to go-micro.dev/v4.
|
||||
|
||||
# 3.7.0 (2021/10/11)
|
||||
|
||||
- Add latest version (#2303).
|
||||
|
||||
# 3.6.0 (2021/08/23)
|
||||
|
||||
- Minor fixes https://github.com/asim/go-micro/compare/v3.5.2...c7195aae9817db4eaf5483990fcb8706f86d3002.
|
||||
|
||||
# 3.5.2 (2021/07/06)
|
||||
|
||||
- Tag it and bag it.
|
||||
|
||||
# 3.5.1 (2021/04/20)
|
||||
|
||||
- Minor bug fixes.
|
||||
|
||||
# 3.5.0 (2021/01/29)
|
||||
|
||||
- kill the bugs.
|
||||
|
||||
# 3.0.1 (2021/01/20)
|
||||
|
||||
- Tag with protoc changes.
|
||||
|
||||
# 3.0.0 (2021/01/20)
|
||||
|
||||
- V3.
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
- upgrade github.com/micro/go-micro/v2 to github.com/asim/go-micro/v3.
|
||||
- change default transport from gRPC to mucp, using grpc server/client plugins.
|
||||
|
||||
# 3.0.0-beta.3 (2020/09/29)
|
||||
|
||||
- Secret implementation of config. Supporting config merge (#2027)
|
||||
- remove transport options
|
||||
- read service package (#2026)
|
||||
- env config implementation (#2024)
|
||||
- runtime: remove builder package (moved to micro) (#2023)
|
||||
- Fix branch names support for k8s runtime (#2020)
|
||||
- fix config bug (#2021)
|
||||
- runtime: minor fixes for local runtime (#2019)
|
||||
- remove memcache and update gomod
|
||||
- Add errors to config methods (#2015)
|
||||
- store/file: fix segmentation violation bug (#2013)
|
||||
- Config interface change (#2010)
|
||||
- proxy/grpc: fix client streaming bug (EOF not sent to the server) (#2011)
|
||||
- client/grpc: fix stream closed bug (#2009)
|
||||
- store/file: don't keep connection to boltdb open (#2006)
|
||||
- runtime/builder with golang implementation (#2003)
|
||||
- store: implement s3 blob store (#2005)
|
||||
- store: add blob interface with file implementation (#2004)
|
||||
- auth: remove micro specific code (#1999)
|
||||
- Fix running subfolders (#1998)
|
||||
- api: fix request body re-sequencing bug (#1996)
|
||||
- add Name to auth.Account as a user friendly alias (#1992)
|
||||
- Fixing top level run outside repo (#1993)
|
||||
- runtime: normalised service statuses (#1994)
|
||||
- Add 'Namespace' header to allowed CORS headers (#1990)
|
||||
- Remove all the external plugins except grpc (#1988)
|
||||
- util/kubernetes: fix TCPSocketAction bug (#1987)
|
||||
- Fixing the metric tagging issue here (#1986).
|
||||
|
||||
# 3.0.0-beta.2 (2020/09/05)
|
||||
|
||||
- Cut a v3 beta 2.
|
||||
|
||||
# 3.0.0-beta (2020/08/12)
|
||||
|
||||
- write nil when expiry is zero.
|
||||
|
||||
# 3.0.0-alpha (2020/07/27)
|
||||
|
||||
- v3 refactor (#1868).
|
||||
|
||||
# 2.9.1 (2020/07/03)
|
||||
|
||||
- push tags to docker hub (#1766).
|
||||
|
||||
# 2.9.0 (2020/06/12)
|
||||
|
||||
- Fix regex detection. Fixes #1663 (#1696).
|
||||
|
||||
# 2.9.0-rc5 (2020/06/11)
|
||||
|
||||
- Merge branch 'master' into release-2.9.0.
|
||||
|
||||
# 2.9.0-rc4 (2020/06/11)
|
||||
|
||||
- Merge branch 'master' into release-2.9.0.
|
||||
|
||||
# 2.9.0-rc1 (2020/06/11)
|
||||
|
||||
- Merge branch 'master' into release-2.9.0.
|
||||
|
||||
# 2.8.0 (2020/05/31)
|
||||
|
||||
- Rewrite Auth interface to use Rules
|
||||
- Add Cache interface into the Client for request caching
|
||||
- Fix atomic sequence updates in Client
|
||||
- Update go mod deps
|
||||
- Fix ipv6 parsing in mdns registry
|
||||
- Add namespacing to the default runtime
|
||||
- Replace go-git with v5
|
||||
- Increase register ttl to 90 seconds.
|
||||
|
||||
# 2.7.0 (2020/05/18)
|
||||
|
||||
- Fix the rpc handler json rpc body parsing
|
||||
- Use caddyserver/certmagic instead of mholt
|
||||
- Add HasRole to Account
|
||||
- Add jwt refresh token generation
|
||||
- Fix rpc stream close locking race
|
||||
- Add auth namespace env var
|
||||
- Strip the router penalty code
|
||||
- Add file upload util
|
||||
- Fix killing processes in runtime
|
||||
- Pass namespace to runtime commands
|
||||
- Generate account on start
|
||||
- Check errors in cockroachdb.
|
||||
|
||||
# 2.6.0 (2020/05/04)
|
||||
|
||||
- Fix discord bot authentication header
|
||||
- Improve api rpc regexp matching
|
||||
- Change auth account access via context
|
||||
- Create a jwt implementation of auth
|
||||
- Fix grpc content-type encoding bug
|
||||
- Consolidate proxy/network env var logic
|
||||
- Change secrets interface naming
|
||||
- Log file path in the logger
|
||||
- Change location of network resolver
|
||||
- Add Store to service options
|
||||
- Fix default runtime log parsing
|
||||
- Add namespace checks to k8s runtime
|
||||
- Add proper git checkout in local runtime
|
||||
- Add database/table options for store
|
||||
- Add pki implementation
|
||||
- Import qson.
|
||||
|
||||
# 2.5.0 (2020/04/15)
|
||||
|
||||
- api/router/registry: extract path based parameters from url to req (#1530).
|
||||
|
||||
# 2.4.0 (2020/03/31)
|
||||
|
||||
- There can be only one! (#1445).
|
||||
|
||||
# 2.3.0 (2020/03/17)
|
||||
|
||||
- grpc client/server fixes (#1355).
|
||||
|
||||
# 2.2.0 (2020/02/28)
|
||||
|
||||
- Rename Auth Validate to Verify
|
||||
- Replaces noop auth with base32 generated tokens
|
||||
- Change Excludes to Exclude
|
||||
- Add token option to auth
|
||||
- Add profile option and flags for debug
|
||||
- Add config loading for auth token
|
||||
- Move before start to before listening.
|
||||
|
||||
# 2.1.2 (2020/02/24)
|
||||
|
||||
- fix router panic (#1254).
|
||||
|
||||
# 2.1.1 (2020/02/23)
|
||||
|
||||
- update go modules (#1240).
|
||||
|
||||
# 2.1.0 (2020/02/13)
|
||||
|
||||
- Exclude Stats & Trace from Auth (#1192).
|
||||
|
||||
# 2.0.0 (2020/01/30)
|
||||
|
||||
- v2 release.
|
||||
|
||||
# 1.18.0 (2019/12/08)
|
||||
|
||||
- Add golang ci linter
|
||||
- Add race detection to travis
|
||||
- Please the linter
|
||||
- Do some perf optimisations on slice alloc
|
||||
- Move http broker to use single entry in registry
|
||||
- Strip the grpc metadata filtering
|
||||
- Strip the old codec usage
|
||||
- Disable retries in client when MICRO_PROXY is enabled
|
||||
- Strip old X-Micro headers
|
||||
- Add debug/log streaming implementations
|
||||
- Add first debug/log interface
|
||||
- Huge network/tunnel refactor to fix bugs
|
||||
- Fix proxy slice allocation bug
|
||||
- Splay out some of the network events
|
||||
- Default to AdvertiseLocal for router
|
||||
- Add runtime filtering with Type
|
||||
- Remove SIGKILL processing.
|
||||
|
||||
# 1.17.1 (2019/11/27)
|
||||
|
||||
- fix rpc server go routine leak
|
||||
- add a psuedo socket pool
|
||||
- update debug buffer to return entries.
|
||||
|
||||
# 1.17.0 (2019/11/27)
|
||||
|
||||
- Add github related issue templates
|
||||
- Add Dockerfile for predownloaded go-micro source
|
||||
- Regenerate all the protos to move to \*.pb.micro.go
|
||||
- Fix api handler to parse text/plain as default content type
|
||||
- Fix event handler to allow GET requests
|
||||
- Change http broker ids to go.micro.http.broker-uuid
|
||||
- Require protocol field in metadata to query services via client
|
||||
- Process raw frames in call to Publish
|
||||
- Complete proxy support for processing messages
|
||||
- Proxy support for publishing of messages
|
||||
- Fix grpc connection leak by always closing the connection
|
||||
- Add a debug ring buffer
|
||||
- Add broker to tunnel and network
|
||||
- Force network dns resolver to use cloudflare 1.0.0.1
|
||||
- Add option to specify whether server should handle signalling
|
||||
- Change mdns request timeout to 10ms rather than 100ms
|
||||
- Add router AdvertiseNone and AdvertiseLocal strategies
|
||||
- Rename runtime packager to builder
|
||||
- Add full support for a kubernetes runtime.
|
||||
|
||||
# 1.16.0 (2019/11/09)
|
||||
|
||||
- Pre-make slices for perf optimisation
|
||||
- Add runtime flag and k8s runtime
|
||||
- Add debug/profile for pprof profiling
|
||||
- Reduce go routines in mdns registry and registry cache
|
||||
- Optimise the router flap detection.
|
||||
|
||||
# 1.15.1 (2019/11/03)
|
||||
|
||||
- Router recovery penalty should be below 500.
|
||||
|
||||
# 1.15.0 (2019/11/03)
|
||||
|
||||
- go fmt -s
|
||||
- web generate service on registration
|
||||
- downgrade some network messages to trace
|
||||
- fix tunnel panic on deleting link
|
||||
- add postgres store
|
||||
- change grpc recover logging
|
||||
- add runtime service
|
||||
- add kubernetes runtime
|
||||
- add runtime notifier
|
||||
- proxy add header based routing for Micro-{Gateway, Router, Network}
|
||||
- network hash address based on service + node id
|
||||
- metadata add mergecontext function.
|
||||
|
||||
# 1.14.0 (2019/10/25)
|
||||
|
||||
- Remove consul registry
|
||||
- Change store Sync endpoint to List
|
||||
- Remove cloudflare-go usage in store
|
||||
- Add non-backwards compatible link changes.
|
||||
|
||||
# 1.13.2 (2019/10/22)
|
||||
|
||||
- Fix proxy selection to use round robin strategy.
|
||||
|
||||
# 1.13.1 (2019/10/19)
|
||||
|
||||
- Fix divide by zero bug in broker.
|
||||
|
||||
# 1.13.0 (2019/10/18)
|
||||
|
||||
- Fix network recursive read lock bug
|
||||
- Add certmagic random pull time
|
||||
- Strip http broker topic: prefix.
|
||||
|
||||
# 1.12.0 (2019/10/17)
|
||||
|
||||
- Add ACME Provider interface
|
||||
- Implement certmagic ACME Provider
|
||||
- Add certmagic Store implementation
|
||||
- Add broker service implementation
|
||||
- Add ability to set grpc dial and call options
|
||||
- Add etcd registry and other plugins
|
||||
- Add Network.Connect rpc endpoint
|
||||
- Resolve network node dns names
|
||||
- Support Network.Routes querying
|
||||
- Fix caching registry bugs
|
||||
- Move gossip registry to go-plugins
|
||||
- Add router advertise strategy
|
||||
- Add Cloudflare store implementation
|
||||
- Add store service implementation.
|
||||
|
||||
# 1.11.3 (2019/10/12)
|
||||
|
||||
- Fix the quic-go checksum mismatch by updating to 0.12.1.
|
||||
|
||||
# 1.11.2 (2019/10/12)
|
||||
|
||||
- Fix cache error check.
|
||||
|
||||
# 1.11.1 (2019/10/07)
|
||||
|
||||
- Fix cache registry deadlocking bug.
|
||||
|
||||
# 1.11.0 (2019/10/01)
|
||||
|
||||
- This is likely the last release of v1.
|
||||
|
||||
# 1.10.0 (2019/09/11)
|
||||
|
||||
- Add grpc client code application/grpc content-type
|
||||
- Move client to use stream dialer
|
||||
- Add network implementation
|
||||
- Add dynamic plugin loading
|
||||
- Add multilink usage in proxy
|
||||
- Add registry implementation
|
||||
- Scope mdns to .micro domain
|
||||
- Support grpc server processing by default
|
||||
- Add tunnel broker.
|
||||
|
||||
# 1.9.1 (2019/08/19)
|
||||
|
||||
- Fix waitgroup race condition.
|
||||
|
||||
# 1.9.0 (2019/08/19)
|
||||
|
||||
- Fix grpc codec for broker publishing
|
||||
- Use the connection pool for streaming
|
||||
- Send EOS from client when closing stream
|
||||
- Add stream header to mucp protocol
|
||||
- Add stream multiplexing in the server
|
||||
- Fix watcher bug in file config source
|
||||
- Fix monitoring watcher to only look at mucp services
|
||||
- Only check router status on lookup failure
|
||||
- Fix proxy streaming and client request processing
|
||||
- Fix host:port processing for messaging systems
|
||||
- Add start method to the router
|
||||
- Fix router race condition for default values
|
||||
- Add loopback detection to the tunnel
|
||||
- Add connection retry logic to tunnel
|
||||
- Make log levels accessible for the logger
|
||||
- Add proxy muxer for internal calls.
|
||||
|
||||
# 1.8.3 (2019/08/12)
|
||||
|
||||
- Fix nats draining
|
||||
- More verbose selector errors to return service name
|
||||
- Move handler debug package
|
||||
- Add a monitoring package
|
||||
- Fix consul address parsing
|
||||
- Fix server extraction code
|
||||
- Add tunnel implementation
|
||||
- Add util log level
|
||||
- Add util io package to wrap transport socket.
|
||||
|
||||
# 1.8.2 (2019/08/06)
|
||||
|
||||
- Point release for micro
|
||||
- Adds travis caching
|
||||
- Removes unused network code
|
||||
- Adds tunnel interface
|
||||
- Consul agent check
|
||||
- Router handler interface
|
||||
- Non host:port fixes.
|
||||
|
||||
# 1.8.1 (2019/07/31)
|
||||
|
||||
- Use mdns 0.2.0 release tag.
|
||||
|
||||
# 1.8.0 (2019/07/29)
|
||||
|
||||
- Move the selector into client
|
||||
- Change broker.Publication to broker.Event
|
||||
- Move cmd into config
|
||||
- Enable default json processing in api
|
||||
- Remove port from registry
|
||||
- Memory broker/transport race fixes
|
||||
- GRPC codec fix
|
||||
- Client pool interface
|
||||
- Router interface/service implementations
|
||||
- Config decoding fixes
|
||||
- Memory store expiration fix
|
||||
- Network link/tunnel/resolver packages
|
||||
- Proxy router caching
|
||||
- Registry util functions.
|
||||
|
||||
# 1.7.0 (2019/06/21)
|
||||
|
||||
- Update go mod
|
||||
- Move mock data out of memory registry
|
||||
- wrap the grpc codecs to support framing
|
||||
- change grpc resolution to use service.method
|
||||
- support full proxying via grpc
|
||||
- add text codec
|
||||
- move data/store
|
||||
- add network interface
|
||||
- add router package and implementation
|
||||
- move options to config/options
|
||||
- send gossip updates on register/deregister
|
||||
- fix node add/del bug
|
||||
- add handler wrapper back into core router.
|
||||
|
||||
# 1.6.0 (2019/06/07)
|
||||
|
||||
- Massive go.mod dependency cleanup _ Moved etcd, memcache, redis sync things to go-plugins _ uuid to google uuid \* blew away go.mod
|
||||
- Add better proxy interface and features
|
||||
- Add new options interface.
|
||||
|
||||
# 1.5.0 (2019/06/05)
|
||||
|
||||
- Fix go mod issues.
|
||||
|
||||
# 1.4.0 (2019/06/04)
|
||||
|
||||
- Final consolidation of all libraries.
|
||||
|
||||
# 1.3.1 (2019/06/03)
|
||||
|
||||
- Fix broken pipe bug. Don't send message when client closed connection..
|
||||
|
||||
# 1.3.0 (2019/05/31)
|
||||
|
||||
- The great rewrite.
|
||||
|
||||
# 1.2.0 (2019/05/22)
|
||||
|
||||
- Update go mod
|
||||
- Fix mock client
|
||||
- Fix retries logic
|
||||
- Fix consul api change
|
||||
- Use consul client for watcher
|
||||
- Fix gossip data races
|
||||
- Add registry check function.
|
||||
|
||||
# 1.1.0 (2019/03/28)
|
||||
|
||||
- Update go mod
|
||||
- Fix endpoint extractor generation.
|
||||
|
||||
# 1.0.0 (2019/03/05)
|
||||
|
||||
- 1.0.0 release.
|
||||
|
||||
# 0.27.1 (2019/03/05)
|
||||
|
||||
- Fix nil consul client.
|
||||
|
||||
# 0.27.0 (2019/02/23)
|
||||
|
||||
- Remove buff check in http transport
|
||||
- Change default version to latest
|
||||
- Add exchange routing
|
||||
- Update go modules.
|
||||
|
||||
# 0.26.1 (2019/02/13)
|
||||
|
||||
- Fix gossip registry
|
||||
- Update go modules for rcache.
|
||||
|
||||
# 0.26.0 (2019/02/13)
|
||||
|
||||
- Update go modules
|
||||
- Add gossip registry rejoin
|
||||
- Move selector to rcache.
|
||||
|
||||
# 0.25.0 (2019/02/04)
|
||||
|
||||
- Add server request body.
|
||||
|
||||
# 0.24.1 (2019/02/01)
|
||||
|
||||
- Various bug fixes
|
||||
- Backwards compatible with 0.14 and older
|
||||
- Fix mdns and gossip race conditions
|
||||
- Use official h2c server
|
||||
- Enable support for MICRO_PROXY.
|
||||
|
||||
# 0.24.0 (2019/01/30)
|
||||
|
||||
- Add go mod.
|
||||
|
||||
# 0.23.0 (2019/01/29)
|
||||
|
||||
- Move headers from X-Micro to Micro
|
||||
- Remove Register/Deregister methods from server
|
||||
- Move register_interval to be internal
|
||||
- Add subscriber context option.
|
||||
|
||||
# 0.22.1 (2019/01/22)
|
||||
|
||||
- Fix broken error handling
|
||||
- now returns error from ServeRequest router.
|
||||
|
||||
# 0.22.0 (2019/01/18)
|
||||
|
||||
- Address backwards compatibility.
|
||||
|
||||
# 0.21.0 (2019/01/17)
|
||||
|
||||
- Make MDNS the default registry
|
||||
- Move mocks to be memory implementations
|
||||
- Add metadata.Copy function.
|
||||
|
||||
# 0.20.0 (2019/01/14)
|
||||
|
||||
- BREAKING CHANGES.
|
||||
|
||||
# 0.17.0 (2019/01/03)
|
||||
|
||||
- Offline inbox for http broker
|
||||
- JSON/Proto/GRPC codecs
|
||||
- HTTP proxy from environment.
|
||||
|
||||
# 0.16.0 (2018/12/29)
|
||||
|
||||
- Fix cache/gossip data race
|
||||
- Rename cache selector to registry.
|
||||
|
||||
# 0.15.1 (2018/12/18)
|
||||
|
||||
- Selector cache lookup optimization.
|
||||
|
||||
# 0.15.0 (2018/12/13)
|
||||
|
||||
- Public NewSubscribeOptions
|
||||
- http2 broker support
|
||||
- Timeout error function
|
||||
- Consul Query Options
|
||||
- Gossip registry
|
||||
- RPC Codec renaming.
|
||||
|
||||
# 0.14.1 (2018/11/22)
|
||||
|
||||
- bug fix socket headers.
|
||||
|
||||
# 0.14.0 (2018/11/21)
|
||||
|
||||
- use google uuid
|
||||
- add http handler option.
|
||||
|
||||
# 0.13.0 (2018/11/15)
|
||||
|
||||
- add local/remote ip methods
|
||||
- various linting things
|
||||
- get checks on 0 ttl
|
||||
- accept loop.
|
||||
|
||||
# 0.12.0 (2018/10/09)
|
||||
|
||||
- reorder server flag
|
||||
- atomic increment sequence
|
||||
- new error method.
|
||||
|
||||
# 0.11.0 (2018/08/24)
|
||||
|
||||
- Support Consul Connect registration
|
||||
- Add/Use Init for initialisation from cmd.
|
||||
|
||||
# 0.10.0 (2018/07/26)
|
||||
|
||||
- Fix broker locking
|
||||
- Add RetryOnError as default retry policy
|
||||
- Fix mock client reflection
|
||||
- Support dialtimeout only above 0
|
||||
- Add verbose client errors
|
||||
- Allow client retries to be 0.
|
||||
|
||||
# 0.9.0 (2018/06/09)
|
||||
|
||||
- Reset server address on shutdown
|
||||
- Set default pool size to 1
|
||||
- Support reinitialising connection pool
|
||||
- Set retries to 1 by default
|
||||
- Return error for subscribers.
|
||||
|
||||
# 0.8.0 (2018/04/20)
|
||||
|
||||
- Rework of interfaces.
|
||||
|
||||
# 0.7.0 (2018/04/10)
|
||||
|
||||
- Move misc to util package
|
||||
- Add register ttl and interval flags
|
||||
- Fix protoc-gen-micro example.
|
||||
|
||||
# 0.6.0 (2018/04/05)
|
||||
|
||||
- Add consul TCP check
|
||||
- Atomic increment rpc stream sequence.
|
||||
|
||||
# 0.5.0 (2018/03/04)
|
||||
|
||||
- Support consul services without version
|
||||
- Switch to stdlib context.
|
||||
|
||||
# 0.4.0 (2018/02/19)
|
||||
|
||||
- Add WatchOption which allows filtering by service
|
||||
- Add Options method to registry
|
||||
- Add Conflict error
|
||||
- Only watch selected services in cache.
|
||||
|
||||
# 0.3.0 (2018/01/02)
|
||||
|
||||
- https support for consul
|
||||
- subscriber deadlock fix
|
||||
- selector top level option.
|
||||
|
||||
# 0.2.0 (2017/10/29)
|
||||
|
||||
- Performance improvements.
|
||||
|
||||
# 0.1.4 (2017/09/04)
|
||||
|
||||
- sort handler/subscriber endpoints
|
||||
- pass options to new subscriber.
|
||||
|
||||
# 0.1.3 (2017/08/15)
|
||||
|
||||
- Bug fix nil consul http client.
|
||||
|
||||
# 0.1.2 (2017/07/20)
|
||||
|
||||
- respond when codec errors out.
|
||||
|
||||
# 0.1.1 (2017/06/12)
|
||||
|
||||
- Fix potential panic/waitgroup bug.
|
||||
|
||||
# 0.1.0 (2017/06/12)
|
||||
|
||||
- Initial release.
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Asim Aslam.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
# Go Micro [](https://opensource.org/licenses/Apache-2.0) [](https://pkg.go.dev/go-micro.dev/v4?tab=doc) [](https://goreportcard.com/report/github.com/go-micro/go-micro)
|
||||
|
||||
Go Micro is a framework for distributed systems development.
|
||||
|
||||
## Overview
|
||||
|
||||
Go Micro provides the core requirements for distributed systems development including RPC and Event driven communication.
|
||||
The Go Micro philosophy is sane defaults with a pluggable architecture. We provide defaults to get you started quickly
|
||||
but everything can be easily swapped out.
|
||||
|
||||
## Features
|
||||
|
||||
Go 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
|
||||
based access control.
|
||||
|
||||
- **Dynamic Config** - Load and hot reload dynamic config from anywhere. The config interface provides a way to load application
|
||||
level config from any source such as env vars, file, etcd. You can merge the sources and even define fallbacks.
|
||||
|
||||
- **Data Storage** - A simple data store interface to read, write and delete records. It includes support for memory, file and
|
||||
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.
|
||||
|
||||
- **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
|
||||
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.
|
||||
|
||||
- **RPC Client/Server** - RPC 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.
|
||||
|
||||
- **Event Streaming** - PubSub is great for async notifications but for more advanced use cases event streaming is preferred. Offering
|
||||
persistent storage, consuming from offsets and acking. Go Micro includes support for NATS Jetstream and Redis streams.
|
||||
|
||||
- **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.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To make use of Go Micro import it
|
||||
|
||||
```golang
|
||||
import "go-micro.dev/v4"
|
||||
```
|
||||
|
||||
Define a handler (protobuf is optionally supported - see [example](https://github.com/go-micro/examples/blob/main/helloworld/main.go))
|
||||
|
||||
```golang
|
||||
type Request struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type Helloworld struct{}
|
||||
|
||||
func (h *Helloworld) Greeting(ctx context.Context, req *Request, rsp *Response) error {
|
||||
rsp.Message = "Hello " + req.Name
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
Create, initialise and run the service
|
||||
|
||||
```golang
|
||||
// create a new service
|
||||
service := micro.NewService(
|
||||
micro.Name("helloworld"),
|
||||
micro.Handle(new(Helloworld)),
|
||||
)
|
||||
|
||||
// initialise flags
|
||||
service.Init()
|
||||
|
||||
// start the service
|
||||
service.Run()
|
||||
```
|
||||
|
||||
Optionally set fixed address
|
||||
|
||||
```golang
|
||||
service := micro.NewService(
|
||||
// set address
|
||||
micro.Address(":8080"),
|
||||
)
|
||||
```
|
||||
|
||||
Call it via curl
|
||||
|
||||
```
|
||||
curl -XPOST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Micro-Endpoint: Helloworld.Greeting' \
|
||||
-d '{"name": "alice"}' \
|
||||
http://localhost:8080
|
||||
```
|
||||
|
||||
See the [examples](https://github.com/go-micro/examples) for detailed information on usage.
|
||||
|
||||
## Toolkit
|
||||
|
||||
See [github.com/go-micro](https://github.com/go-micro) for tooling.
|
||||
|
||||
- [API](https://github.com/go-micro/api)
|
||||
- [CLI](https://github.com/go-micro/cli)
|
||||
- [Demo](https://github.com/go-micro/demo)
|
||||
- [Plugins](https://github.com/go-micro/plugins)
|
||||
- [Examples](https://github.com/go-micro/examples)
|
||||
- [Dashboard](https://github.com/go-micro/dashboard)
|
||||
- [Generator](https://github.com/go-micro/generator)
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG.md](https://github.com/micro/go-micro/tree/master/CHANGELOG.md) for release history.
|
||||
|
||||
## License
|
||||
|
||||
Go Micro is Apache 2.0 licensed.
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
// Package api is for building api gateways
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v4/api/router"
|
||||
"go-micro.dev/v4/client"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/server"
|
||||
)
|
||||
|
||||
// API interface provides a way to
|
||||
// create composable API gateways.
|
||||
type Api interface {
|
||||
// Initialize options
|
||||
Init(...Option) error
|
||||
// Get the options
|
||||
Options() Options
|
||||
// Register an endpoint
|
||||
Register(*Endpoint) error
|
||||
// Deregister an endpoint
|
||||
Deregister(*Endpoint) error
|
||||
// Run the api
|
||||
Run(context.Context) error
|
||||
// Implemenation of api e.g http
|
||||
String() string
|
||||
}
|
||||
|
||||
// Options are API options.
|
||||
type Options struct {
|
||||
// Router for resolving routes
|
||||
Router router.Router
|
||||
// Client to use for RPC
|
||||
Client client.Client
|
||||
// Address of the server
|
||||
Address string
|
||||
}
|
||||
|
||||
// Option type are API option args.
|
||||
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 string
|
||||
// Description e.g what's this endpoint for
|
||||
Description string
|
||||
// API 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
|
||||
// Stream flag
|
||||
Stream bool
|
||||
}
|
||||
|
||||
// Service represents an API service.
|
||||
type Service struct {
|
||||
// Name of service
|
||||
Name string
|
||||
// The endpoint for this service
|
||||
Endpoint *Endpoint
|
||||
// Versions of this service
|
||||
Versions []*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
|
||||
}
|
||||
|
||||
// Encode encodes an endpoint to endpoint metadata.
|
||||
func Encode(e *Endpoint) map[string]string {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// endpoint map
|
||||
em := make(map[string]string)
|
||||
|
||||
// set vals only if they exist
|
||||
set := func(k, v string) {
|
||||
if len(v) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
em[k] = v
|
||||
}
|
||||
|
||||
set("endpoint", e.Name)
|
||||
set("description", e.Description)
|
||||
set("handler", e.Handler)
|
||||
set("method", strings.Join(e.Method, ","))
|
||||
set("path", strings.Join(e.Path, ","))
|
||||
set("host", strings.Join(e.Host, ","))
|
||||
|
||||
return em
|
||||
}
|
||||
|
||||
// Decode decodes endpoint metadata into an endpoint.
|
||||
func Decode(e map[string]string) *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"],
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates an endpoint to guarantee it won't blow up when being served.
|
||||
func Validate(e *Endpoint) error {
|
||||
if e == nil {
|
||||
return errors.New("endpoint is nil")
|
||||
}
|
||||
|
||||
if len(e.Name) == 0 {
|
||||
return errors.New("name required")
|
||||
}
|
||||
|
||||
for _, p := range e.Path {
|
||||
ps := p[0]
|
||||
pe := p[len(p)-1]
|
||||
|
||||
if ps == '^' && pe == '$' {
|
||||
_, err := regexp.CompilePOSIX(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if ps == '^' && pe != '$' {
|
||||
return errors.New("invalid path")
|
||||
} else if ps != '^' && pe == '$' {
|
||||
return errors.New("invalid path")
|
||||
}
|
||||
}
|
||||
|
||||
if len(e.Handler) == 0 {
|
||||
return errors.New("invalid handler")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithEndpoint returns a server.HandlerOption with endpoint metadata set
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// proto.RegisterHandler(service.Server(), new(Handler), api.WithEndpoint(
|
||||
// &api.Endpoint{
|
||||
// Name: "Greeter.Hello",
|
||||
// Path: []string{"/greeter"},
|
||||
// },
|
||||
// ))
|
||||
func WithEndpoint(e *Endpoint) server.HandlerOption {
|
||||
return server.EndpointMetadata(e.Name, Encode(e))
|
||||
}
|
||||
|
||||
// NewApi returns a new api gateway.
|
||||
func NewApi(opts ...Option) Api {
|
||||
return newApi(opts...)
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/api/handler"
|
||||
"go-micro.dev/v4/api/handler/rpc"
|
||||
"go-micro.dev/v4/api/router/registry"
|
||||
"go-micro.dev/v4/api/server"
|
||||
"go-micro.dev/v4/api/server/http"
|
||||
)
|
||||
|
||||
type api struct {
|
||||
options Options
|
||||
|
||||
server server.Server
|
||||
}
|
||||
|
||||
func newApi(opts ...Option) Api {
|
||||
options := NewOptions(opts...)
|
||||
|
||||
rtr := options.Router
|
||||
|
||||
if rtr == nil {
|
||||
// TODO: make configurable
|
||||
rtr = registry.NewRouter()
|
||||
}
|
||||
|
||||
// TODO: make configurable
|
||||
hdlr := rpc.NewHandler(
|
||||
handler.WithRouter(rtr),
|
||||
)
|
||||
|
||||
// TODO: make configurable
|
||||
// create a new server
|
||||
srv := http.NewServer(options.Address)
|
||||
|
||||
// TODO: allow multiple handlers
|
||||
// define the handler
|
||||
srv.Handle("/", hdlr)
|
||||
|
||||
return &api{
|
||||
options: options,
|
||||
server: srv,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *api) Init(opts ...Option) error {
|
||||
for _, o := range opts {
|
||||
o(&a.options)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the options.
|
||||
func (a *api) Options() Options {
|
||||
return a.options
|
||||
}
|
||||
|
||||
// Register a http handler.
|
||||
func (a *api) Register(*Endpoint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register a route.
|
||||
func (a *api) Deregister(*Endpoint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *api) Run(ctx context.Context) error {
|
||||
if err := a.server.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// wait to finish
|
||||
<-ctx.Done()
|
||||
|
||||
if err := a.server.Stop(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *api) String() string {
|
||||
return "http"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Package handler provides http handlers
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Handler represents a HTTP handler that manages a request.
|
||||
type Handler interface {
|
||||
// standard http handler
|
||||
http.Handler
|
||||
// name of handler
|
||||
String() string
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"go-micro.dev/v4/api/router"
|
||||
"go-micro.dev/v4/client"
|
||||
"go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultMaxRecvSize is 10MiB.
|
||||
DefaultMaxRecvSize int64 = 1024 * 1024 * 100
|
||||
)
|
||||
|
||||
// Options is the list of api Options.
|
||||
type Options struct {
|
||||
Router router.Router
|
||||
Client client.Client
|
||||
Logger logger.Logger
|
||||
Namespace string
|
||||
MaxRecvSize int64
|
||||
}
|
||||
|
||||
// Option is a api Option.
|
||||
type Option func(o *Options)
|
||||
|
||||
// NewOptions fills in the blanks.
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if options.Client == nil {
|
||||
WithClient(client.DefaultClient)(&options)
|
||||
}
|
||||
|
||||
if options.MaxRecvSize == 0 {
|
||||
options.MaxRecvSize = DefaultMaxRecvSize
|
||||
}
|
||||
|
||||
if options.Logger == nil {
|
||||
options.Logger = logger.LoggerOrDefault(options.Logger)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// WithNamespace specifies the namespace for the handler.
|
||||
func WithNamespace(s string) Option {
|
||||
return func(o *Options) {
|
||||
o.Namespace = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithRouter specifies a router to be used by the handler.
|
||||
func WithRouter(r router.Router) Option {
|
||||
return func(o *Options) {
|
||||
o.Router = r
|
||||
}
|
||||
}
|
||||
|
||||
// WithClient sets the client for the handler.
|
||||
func WithClient(c client.Client) Option {
|
||||
return func(o *Options) {
|
||||
o.Client = c
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxRecvSize specifies max body size.
|
||||
func WithMaxRecvSize(size int64) Option {
|
||||
return func(o *Options) {
|
||||
o.MaxRecvSize = size
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger specifies the logger.
|
||||
func WithLogger(l logger.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = l
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
// Package rpc is a go-micro rpc handler.
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch/v5"
|
||||
"github.com/oxtoacart/bpool"
|
||||
"go-micro.dev/v4/api/handler"
|
||||
"go-micro.dev/v4/api/internal/proto"
|
||||
"go-micro.dev/v4/api/router"
|
||||
"go-micro.dev/v4/client"
|
||||
"go-micro.dev/v4/codec"
|
||||
"go-micro.dev/v4/codec/jsonrpc"
|
||||
"go-micro.dev/v4/codec/protorpc"
|
||||
"go-micro.dev/v4/errors"
|
||||
log "go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/metadata"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/selector"
|
||||
"go-micro.dev/v4/util/ctx"
|
||||
"go-micro.dev/v4/util/qson"
|
||||
)
|
||||
|
||||
const (
|
||||
// Handler is the name of this handler.
|
||||
Handler = "rpc"
|
||||
packageID = "go.micro.api"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type buffer struct {
|
||||
io.ReadCloser
|
||||
}
|
||||
|
||||
func (b *buffer) Write(_ []byte) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// strategy is a hack for selection.
|
||||
func strategy(services []*registry.Service) selector.Strategy {
|
||||
return func(_ []*registry.Service) selector.Next {
|
||||
// ignore input to this function, use services above
|
||||
return selector.Random(services)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
logger := h.opts.Logger
|
||||
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 *router.Route
|
||||
|
||||
if h.opts.Router != nil {
|
||||
// try get service from router
|
||||
s, err := h.opts.Router.Route(r)
|
||||
if err != nil {
|
||||
werr := writeError(w, r, errors.InternalServerError(packageID, err.Error()))
|
||||
if werr != nil {
|
||||
logger.Log(log.ErrorLevel, werr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
service = s
|
||||
} else {
|
||||
// we have no way of routing the request
|
||||
werr := writeError(w, r, errors.InternalServerError(packageID, "no route found"))
|
||||
if werr != nil {
|
||||
logger.Log(log.ErrorLevel, werr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
|
||||
// Strip charset from Content-Type (like `application/json; charset=UTF-8`)
|
||||
if idx := strings.IndexRune(contentType, ';'); idx >= 0 {
|
||||
contentType = contentType[:idx]
|
||||
}
|
||||
|
||||
// micro client
|
||||
myClient := h.opts.Client
|
||||
|
||||
// create context
|
||||
myContext := ctx.FromRequest(r)
|
||||
// get context from http handler wrappers
|
||||
md, ok := metadata.FromContext(r.Context())
|
||||
if !ok {
|
||||
md = make(metadata.Metadata)
|
||||
}
|
||||
// fill contex with http headers
|
||||
md["Host"] = r.Host
|
||||
md["Method"] = r.Method
|
||||
// get canonical headers
|
||||
for k := range r.Header {
|
||||
// may be need to get all values for key like r.Header.Values() provide in go 1.14
|
||||
md[textproto.CanonicalMIMEHeaderKey(k)] = r.Header.Get(k)
|
||||
}
|
||||
|
||||
// merge context with overwrite
|
||||
myContext = metadata.MergeContext(myContext, md, true)
|
||||
|
||||
// set merged context to request
|
||||
*r = *r.Clone(myContext)
|
||||
// if stream we currently only support json
|
||||
if isStream(r, service) {
|
||||
// drop older context as it can have timeouts and create new
|
||||
// md, _ := metadata.FromContext(cx)
|
||||
// serveWebsocket(context.TODO(), w, r, service, c)
|
||||
if err := serveWebsocket(myContext, w, r, service, myClient); err != nil {
|
||||
logger.Log(log.ErrorLevel, err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// create strategy
|
||||
mySelector := selector.WithStrategy(strategy(service.Versions))
|
||||
|
||||
// walk the standard call path
|
||||
// get payload
|
||||
br, err := requestPayload(r)
|
||||
if err != nil {
|
||||
if werr := writeError(w, r, err); werr != nil {
|
||||
logger.Log(log.ErrorLevel, werr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var rsp []byte
|
||||
|
||||
switch {
|
||||
// proto codecs
|
||||
case hasCodec(contentType, 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 := myClient.NewRequest(
|
||||
service.Service,
|
||||
service.Endpoint.Name,
|
||||
request,
|
||||
client.WithContentType(contentType),
|
||||
)
|
||||
|
||||
// make the call
|
||||
if err := myClient.Call(myContext, req, response, client.WithSelectOption(mySelector)); err != nil {
|
||||
if werr := writeError(w, r, err); werr != nil {
|
||||
logger.Log(log.ErrorLevel, werr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// marshall response
|
||||
rsp, err = response.Marshal()
|
||||
if err != nil {
|
||||
if werr := writeError(w, r, err); werr != nil {
|
||||
logger.Log(log.ErrorLevel, werr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
// if json codec is not present set to json
|
||||
if !hasCodec(contentType, jsonCodecs) {
|
||||
contentType = "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 := myClient.NewRequest(
|
||||
service.Service,
|
||||
service.Endpoint.Name,
|
||||
&request,
|
||||
client.WithContentType(contentType),
|
||||
)
|
||||
// make the call
|
||||
if err := myClient.Call(myContext, req, &response, client.WithSelectOption(mySelector)); err != nil {
|
||||
if werr := writeError(w, r, err); werr != nil {
|
||||
logger.Log(log.ErrorLevel, werr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// marshall response
|
||||
rsp, err = response.MarshalJSON()
|
||||
if err != nil {
|
||||
if werr := writeError(w, r, err); werr != nil {
|
||||
logger.Log(log.ErrorLevel, werr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// write the response
|
||||
if err := writeResponse(w, r, rsp); err != nil {
|
||||
logger.Log(log.ErrorLevel, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *rpcHandler) String() string {
|
||||
return Handler
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
myCt := r.Header.Get("Content-Type")
|
||||
|
||||
switch {
|
||||
case strings.Contains(myCt, "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(myCt, "application/proto-rpc"), strings.Contains(myCt, "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(myCt, "application/www-x-form-urlencoded"), strings.Contains(myCt, "application/x-www-form-urlencoded"):
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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 meadata.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 http.MethodGet:
|
||||
// empty response
|
||||
if strings.Contains(myCt, "application/json") && string(out) == "{}" {
|
||||
return out, nil
|
||||
} else if string(out) == "{}" && !strings.Contains(myCt, "application/json") {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
return out, nil
|
||||
case http.MethodPatch, http.MethodPost, http.MethodPut, http.MethodDelete:
|
||||
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 behavior
|
||||
dstmap[ps[0]] = bodybuf
|
||||
}
|
||||
} else {
|
||||
em := make(map[string]interface{})
|
||||
if jsonbody != nil {
|
||||
em[ps[len(ps)-1]] = jsonbody
|
||||
} else {
|
||||
// old unexpected behavior
|
||||
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
|
||||
}
|
||||
|
||||
return bodybuf, nil
|
||||
}
|
||||
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func writeError(rsp http.ResponseWriter, req *http.Request, err error) error {
|
||||
ce := errors.Parse(err.Error())
|
||||
|
||||
switch ce.Code {
|
||||
case 0:
|
||||
// assuming it's totally screwed
|
||||
ce.Code = http.StatusInternalServerError
|
||||
ce.Id = packageID
|
||||
ce.Status = http.StatusText(http.StatusInternalServerError)
|
||||
ce.Detail = "error during request: " + ce.Detail
|
||||
|
||||
rsp.WriteHeader(http.StatusInternalServerError)
|
||||
default:
|
||||
rsp.WriteHeader(int(ce.Code))
|
||||
}
|
||||
|
||||
// response content type
|
||||
rsp.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Set trailers
|
||||
if strings.Contains(req.Header.Get("Content-Type"), "application/grpc") {
|
||||
rsp.Header().Set("Trailer", "grpc-status")
|
||||
rsp.Header().Set("Trailer", "grpc-message")
|
||||
rsp.Header().Set("grpc-status", "13")
|
||||
rsp.Header().Set("grpc-message", ce.Detail)
|
||||
}
|
||||
|
||||
_, werr := rsp.Write([]byte(ce.Error()))
|
||||
|
||||
return werr
|
||||
}
|
||||
|
||||
func writeResponse(w http.ResponseWriter, r *http.Request, rsp []byte) error {
|
||||
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)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// NewHandler returns a new RPC handler.
|
||||
func NewHandler(opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
|
||||
return &rpcHandler{
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
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"
|
||||
"go-micro.dev/v4/api/router"
|
||||
"go-micro.dev/v4/client"
|
||||
raw "go-micro.dev/v4/codec/bytes"
|
||||
"go-micro.dev/v4/selector"
|
||||
)
|
||||
|
||||
// serveWebsocket will stream rpc back over websockets assuming json.
|
||||
func serveWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, service *router.Route, c client.Client) (err error) {
|
||||
var opCode ws.OpCode
|
||||
|
||||
myCt := r.Header.Get("Content-Type")
|
||||
// Strip charset from Content-Type (like `application/json; charset=UTF-8`)
|
||||
if idx := strings.IndexRune(myCt, ';'); idx >= 0 {
|
||||
myCt = myCt[:idx]
|
||||
}
|
||||
|
||||
// check proto from request
|
||||
switch myCt {
|
||||
case "application/json":
|
||||
opCode = ws.OpText
|
||||
default:
|
||||
opCode = ws.OpBinary
|
||||
}
|
||||
|
||||
hdr := make(http.Header)
|
||||
|
||||
if proto, ok := r.Header["Sec-Websocket-Protocol"]; ok {
|
||||
for _, p := range proto {
|
||||
if p == "binary" {
|
||||
hdr["Sec-WebSocket-Protocol"] = []string{"binary"}
|
||||
opCode = ws.OpBinary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
payload, err := requestPayload(r)
|
||||
if err != nil {
|
||||
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, uRw, _, err := upgrader.Upgrade(r, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if cErr := conn.Close(); cErr != nil && err == nil {
|
||||
err = cErr
|
||||
}
|
||||
}()
|
||||
|
||||
var request interface{}
|
||||
|
||||
if !bytes.Equal(payload, []byte(`{}`)) {
|
||||
switch myCt {
|
||||
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 myCt == "" {
|
||||
myCt = "application/json"
|
||||
}
|
||||
|
||||
req := c.NewRequest(
|
||||
service.Service,
|
||||
service.Endpoint.Name,
|
||||
request,
|
||||
client.WithContentType(myCt),
|
||||
client.StreamingRequest(),
|
||||
)
|
||||
|
||||
cCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
so := selector.WithStrategy(strategy(service.Versions))
|
||||
|
||||
// create a new stream
|
||||
stream, err := c.Stream(cCtx, req, client.WithSelectOption(so))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if request != nil {
|
||||
if err = stream.Send(request); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
if wErr := writeLoop(uRw, stream); wErr != nil && err == nil {
|
||||
err = wErr
|
||||
}
|
||||
}()
|
||||
|
||||
rsp := stream.Response()
|
||||
|
||||
// receive from stream and send to client
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-stream.Context().Done():
|
||||
return nil
|
||||
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 nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// write the response
|
||||
if err = wsutil.WriteServerMessage(uRw, opCode, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uRw.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeLoop.
|
||||
func writeLoop(rw io.ReadWriter, stream client.Stream) error {
|
||||
// close stream when done
|
||||
defer stream.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stream.Context().Done():
|
||||
return nil
|
||||
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 nil
|
||||
case ws.StatusNormalClosure, ws.StatusNoStatusRcvd:
|
||||
// this happens when user close ws connection, or we don't get any status
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isStream(r *http.Request, srv *router.Route) bool {
|
||||
// check if it's a web socket
|
||||
if !isWebSocket(r) {
|
||||
return false
|
||||
}
|
||||
// check if the endpoint supports streaming
|
||||
for _, service := range srv.Versions {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"go-micro.dev/v4/api/router"
|
||||
registry2 "go-micro.dev/v4/api/router/registry"
|
||||
"go-micro.dev/v4/client"
|
||||
"go-micro.dev/v4/registry"
|
||||
)
|
||||
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{
|
||||
Address: ":8080",
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// WithAddress sets the address to listen
|
||||
func WithAddress(addr string) Option {
|
||||
return func(o *Options) error {
|
||||
o.Address = addr
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRouter sets the router to use e.g static or registry.
|
||||
func WithRouter(r router.Router) Option {
|
||||
return func(o *Options) error {
|
||||
o.Router = r
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRegistry sets the api's client and router to use registry.
|
||||
func WithRegistry(r registry.Registry) Option {
|
||||
return func(o *Options) error {
|
||||
o.Client = client.NewClient(client.Registry(r))
|
||||
o.Router = registry2.NewRouter(router.WithRegistry(r))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// NewOptions wires options together.
|
||||
func NewOptions(opts ...Option) Options {
|
||||
var options Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if options.Namespace == nil {
|
||||
options.Namespace = StaticNamespace("go.micro")
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// WithHandler sets the handler being used.
|
||||
func WithHandler(h string) Option {
|
||||
return func(o *Options) {
|
||||
o.Handler = h
|
||||
}
|
||||
}
|
||||
|
||||
// WithNamespace sets the function which determines the namespace for a request.
|
||||
func WithNamespace(n func(*http.Request) string) Option {
|
||||
return func(o *Options) {
|
||||
o.Namespace = n
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Package resolver resolves a http request to an endpoint
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrInvalidPath = errors.New("invalid path")
|
||||
)
|
||||
|
||||
// Resolver resolves requests to endpoints.
|
||||
type Resolver interface {
|
||||
Resolve(r *http.Request) (*Endpoint, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
// Endpoint is the endpoint for a http request.
|
||||
type Endpoint struct {
|
||||
// e.g greeter
|
||||
Name string
|
||||
// HTTP Host e.g example.com
|
||||
Host string
|
||||
// HTTP Methods e.g GET, POST
|
||||
Method string
|
||||
// HTTP Path e.g /greeter.
|
||||
Path string
|
||||
}
|
||||
|
||||
// Options is a struct of available options.
|
||||
type Options struct {
|
||||
Namespace func(*http.Request) string
|
||||
Handler string
|
||||
}
|
||||
|
||||
// Option is a helper for a single option.
|
||||
type Option func(o *Options)
|
||||
|
||||
// StaticNamespace returns the same namespace for each request.
|
||||
func StaticNamespace(ns string) func(*http.Request) string {
|
||||
return func(*http.Request) string {
|
||||
return ns
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Package vpath resolves using http path and recognized versioned urls
|
||||
package vpath
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v4/api/resolver"
|
||||
)
|
||||
|
||||
// NewResolver returns a new vpath resolver.
|
||||
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
||||
return &Resolver{opts: resolver.NewOptions(opts...)}
|
||||
}
|
||||
|
||||
// Resolver is a vpath resolver.
|
||||
type Resolver struct {
|
||||
opts resolver.Options
|
||||
}
|
||||
|
||||
var (
|
||||
re = regexp.MustCompile("^v[0-9]+$")
|
||||
)
|
||||
|
||||
// Resolve resolves a http.Request to an grpc Endpoint.
|
||||
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
||||
if req.URL.Path == "/" {
|
||||
return nil, errors.New("unknown name")
|
||||
}
|
||||
|
||||
parts := strings.Split(req.URL.Path[1:], "/")
|
||||
if len(parts) == 1 {
|
||||
return &resolver.Endpoint{
|
||||
Name: r.withNamespace(req, parts...),
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// /v1/foo
|
||||
if re.MatchString(parts[0]) {
|
||||
return &resolver.Endpoint{
|
||||
Name: r.withNamespace(req, parts[0:2]...),
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &resolver.Endpoint{
|
||||
Name: r.withNamespace(req, parts[0]),
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) String() string {
|
||||
return "path"
|
||||
}
|
||||
|
||||
func (r *Resolver) withNamespace(req *http.Request, parts ...string) string {
|
||||
ns := r.opts.Namespace(req)
|
||||
if len(ns) == 0 {
|
||||
return strings.Join(parts, ".")
|
||||
}
|
||||
|
||||
return strings.Join(append([]string{ns}, parts...), ".")
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Encode encodes an endpoint to endpoint metadata.
|
||||
func Encode(e *Endpoint) map[string]string {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// endpoint map
|
||||
ep := make(map[string]string)
|
||||
|
||||
// set vals only if they exist
|
||||
set := func(k, v string) {
|
||||
if len(v) == 0 {
|
||||
return
|
||||
}
|
||||
ep[k] = v
|
||||
}
|
||||
|
||||
set("endpoint", e.Name)
|
||||
set("description", e.Description)
|
||||
set("handler", e.Handler)
|
||||
set("method", strings.Join(e.Method, ","))
|
||||
set("path", strings.Join(e.Path, ","))
|
||||
set("host", strings.Join(e.Host, ","))
|
||||
|
||||
return ep
|
||||
}
|
||||
|
||||
// Decode decodes endpoint metadata into an endpoint.
|
||||
func Decode(e map[string]string) *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"],
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates an endpoint to guarantee it won't blow up when being served.
|
||||
func Validate(e *Endpoint) error {
|
||||
if e == nil {
|
||||
return errors.New("endpoint is nil")
|
||||
}
|
||||
|
||||
if len(e.Name) == 0 {
|
||||
return errors.New("name required")
|
||||
}
|
||||
|
||||
for _, p := range e.Path {
|
||||
ps := p[0]
|
||||
pe := p[len(p)-1]
|
||||
|
||||
if ps == '^' && pe == '$' {
|
||||
_, err := regexp.CompilePOSIX(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if ps == '^' && pe != '$' {
|
||||
return errors.New("invalid path")
|
||||
} else if ps != '^' && pe == '$' {
|
||||
return errors.New("invalid path")
|
||||
}
|
||||
}
|
||||
|
||||
if len(e.Handler) == 0 {
|
||||
return errors.New("invalid handler")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"go-micro.dev/v4/api/resolver"
|
||||
"go-micro.dev/v4/api/resolver/vpath"
|
||||
"go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/registry"
|
||||
)
|
||||
|
||||
// Options is a struct of options available.
|
||||
type Options struct {
|
||||
Registry registry.Registry
|
||||
Resolver resolver.Resolver
|
||||
Logger logger.Logger
|
||||
Handler string
|
||||
}
|
||||
|
||||
// Option is a helper for a single options.
|
||||
type Option func(o *Options)
|
||||
|
||||
// NewOptions wires options together.
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{
|
||||
Handler: "meta",
|
||||
Registry: registry.DefaultRegistry,
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if options.Resolver == nil {
|
||||
options.Resolver = vpath.NewResolver(
|
||||
resolver.WithHandler(options.Handler),
|
||||
)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func WithHandler(h string) Option {
|
||||
return func(o *Options) {
|
||||
o.Handler = h
|
||||
}
|
||||
}
|
||||
|
||||
func WithRegistry(r registry.Registry) Option {
|
||||
return func(o *Options) {
|
||||
o.Registry = r
|
||||
}
|
||||
}
|
||||
|
||||
func WithResolver(r resolver.Resolver) Option {
|
||||
return func(o *Options) {
|
||||
o.Resolver = r
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger sets the underline logger.
|
||||
func WithLogger(l logger.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = l
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
// Package registry provides a dynamic api service router
|
||||
package registry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v4/api/router"
|
||||
"go-micro.dev/v4/api/router/util"
|
||||
log "go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/metadata"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/registry/cache"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
opts router.Options
|
||||
|
||||
// registry cache
|
||||
rc cache.Cache
|
||||
|
||||
exit chan bool
|
||||
eps map[string]*router.Route
|
||||
// compiled regexp for host and path
|
||||
ceps map[string]*endpoint
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (r *registryRouter) isStopped() bool {
|
||||
select {
|
||||
case <-r.exit:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// refresh list of api services.
|
||||
func (r *registryRouter) refresh() {
|
||||
var attempts int
|
||||
|
||||
logger := r.Options().Logger
|
||||
|
||||
for {
|
||||
services, err := r.opts.Registry.ListServices()
|
||||
if err != nil {
|
||||
attempts++
|
||||
|
||||
logger.Logf(log.ErrorLevel, "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 {
|
||||
logger.Logf(log.ErrorLevel, "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) {
|
||||
logger := r.Options().Logger
|
||||
// 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 {
|
||||
logger.Logf(log.ErrorLevel, "unable to get service: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// update our local endpoints
|
||||
r.store(service)
|
||||
}
|
||||
|
||||
// store local endpoint cache.
|
||||
func (r *registryRouter) store(services []*registry.Service) {
|
||||
logger := r.Options().Logger
|
||||
// endpoints
|
||||
eps := map[string]*router.Route{}
|
||||
|
||||
// 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 := router.Decode(sep.Metadata)
|
||||
|
||||
// if we got nothing skip
|
||||
if err := router.Validate(end); err != nil {
|
||||
logger.Logf(log.TraceLevel, "endpoint validation failed: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// try get endpoint
|
||||
ep, ok := eps[key]
|
||||
if !ok {
|
||||
ep = &router.Route{Service: service.Name}
|
||||
}
|
||||
|
||||
// overwrite the endpoint
|
||||
ep.Endpoint = end
|
||||
// append services
|
||||
ep.Versions = append(ep.Versions, service)
|
||||
// store it
|
||||
eps[key] = ep
|
||||
}
|
||||
}
|
||||
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
// delete any existing eps for services we know
|
||||
for key, route := range r.eps {
|
||||
// skip what we don't care about
|
||||
if !names[route.Service] {
|
||||
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 {
|
||||
logger.Logf(log.TraceLevel, "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 {
|
||||
logger.Logf(log.TraceLevel, "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, "", util.PatternLogger(logger))
|
||||
if err != nil {
|
||||
logger.Logf(log.TraceLevel, "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
|
||||
|
||||
logger := r.Options().Logger
|
||||
|
||||
for {
|
||||
if r.isStopped() {
|
||||
return
|
||||
}
|
||||
|
||||
// watch for changes
|
||||
w, err := r.opts.Registry.Watch()
|
||||
if err != nil {
|
||||
attempts++
|
||||
|
||||
logger.Logf(log.ErrorLevel, "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 {
|
||||
logger.Logf(log.ErrorLevel, "error getting next endoint: %v", err)
|
||||
close(ch)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
r.process(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *registryRouter) Options() router.Options {
|
||||
return r.opts
|
||||
}
|
||||
|
||||
func (r *registryRouter) Stop() error {
|
||||
select {
|
||||
case <-r.exit:
|
||||
return nil
|
||||
default:
|
||||
close(r.exit)
|
||||
r.rc.Stop()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registryRouter) Register(ep *router.Route) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registryRouter) Deregister(ep *router.Route) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registryRouter) Endpoint(req *http.Request) (*router.Route, error) {
|
||||
logger := r.Options().Logger
|
||||
|
||||
if r.isStopped() {
|
||||
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, endpoint := range r.eps {
|
||||
cep, ok := r.ceps[n]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
ep := endpoint.Endpoint
|
||||
|
||||
var mMatch, hMatch, pMatch bool
|
||||
// 1. try method
|
||||
for _, m := range ep.Method {
|
||||
if m == req.Method {
|
||||
mMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !mMatch {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Logf(log.DebugLevel, "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
|
||||
}
|
||||
|
||||
logger.Logf(log.DebugLevel, "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 {
|
||||
logger.Logf(log.DebugLevel, "api gpath not match %s != %v", path, pathreg)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Logf(log.DebugLevel, "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
|
||||
}
|
||||
|
||||
*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) {
|
||||
logger.Logf(log.DebugLevel, "api pcre path not match %s != %v", path, pathreg)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Logf(log.DebugLevel, "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 endpoint, nil
|
||||
}
|
||||
|
||||
// no match
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
|
||||
func (r *registryRouter) Route(req *http.Request) (*router.Route, error) {
|
||||
if r.isStopped() {
|
||||
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
|
||||
rsp, err := r.opts.Resolver.Resolve(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// service name
|
||||
name := rsp.Name
|
||||
|
||||
// get service
|
||||
services, err := r.rc.GetService(name)
|
||||
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"
|
||||
}
|
||||
|
||||
// extract endpoint from Path, case-sensitive
|
||||
// just test it in this case, maybe should put the code somewhere else
|
||||
ep_name := rsp.Method
|
||||
comps := strings.Split(rsp.Path, "/")
|
||||
switch len(comps) {
|
||||
case 3:
|
||||
ep_name = comps[1] + "." + comps[2]
|
||||
case 4:
|
||||
ep_name = comps[2] + "." + comps[3]
|
||||
}
|
||||
|
||||
// construct api service
|
||||
return &router.Route{
|
||||
Service: name,
|
||||
Endpoint: &router.Endpoint{
|
||||
Name: ep_name,
|
||||
Handler: handler,
|
||||
},
|
||||
Versions: services,
|
||||
}, nil
|
||||
// http handler
|
||||
case "http", "proxy", "web":
|
||||
// construct api service
|
||||
return &router.Route{
|
||||
Service: name,
|
||||
Endpoint: &router.Endpoint{
|
||||
Name: req.URL.String(),
|
||||
Handler: r.opts.Handler,
|
||||
Host: []string{req.Host},
|
||||
Method: []string{req.Method},
|
||||
Path: []string{req.URL.Path},
|
||||
},
|
||||
Versions: services,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown handler")
|
||||
}
|
||||
|
||||
func newRouter(opts ...router.Option) *registryRouter {
|
||||
options := router.NewOptions(opts...)
|
||||
r := ®istryRouter{
|
||||
exit: make(chan bool),
|
||||
opts: options,
|
||||
rc: cache.New(options.Registry),
|
||||
eps: make(map[string]*router.Route),
|
||||
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...)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Package router provides api service routing
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go-micro.dev/v4/registry"
|
||||
)
|
||||
|
||||
// Router is used to determine an endpoint for a request.
|
||||
type Router interface {
|
||||
// Returns options
|
||||
Options() Options
|
||||
// Register endpoint in router
|
||||
Register(r *Route) error
|
||||
// Deregister endpoint from router
|
||||
Deregister(r *Route) error
|
||||
// Route returns an api.Service route
|
||||
Route(r *http.Request) (*Route, error)
|
||||
// Stop the router
|
||||
Stop() error
|
||||
}
|
||||
|
||||
type Route struct {
|
||||
// Name of service
|
||||
Service string
|
||||
// The endpoint for this service
|
||||
Endpoint *Endpoint
|
||||
// Versions of this service
|
||||
Versions []*registry.Service
|
||||
}
|
||||
|
||||
// Endpoint is a mapping between an RPC method and HTTP endpoint.
|
||||
type Endpoint struct {
|
||||
// RPC Method e.g. Greeter.Hello
|
||||
Name string
|
||||
// What the endpoint is for
|
||||
Description string
|
||||
// API 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
|
||||
// Stream flag
|
||||
Stream bool
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2015, Gengo, Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of Gengo, Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,122 @@
|
||||
package util
|
||||
|
||||
// download from
|
||||
// https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/compile.go
|
||||
|
||||
const (
|
||||
opcodeVersion = 1
|
||||
)
|
||||
|
||||
// Template is a compiled representation of path templates.
|
||||
type Template struct {
|
||||
// Verb is a VERB part in the template.
|
||||
Verb string
|
||||
// Original template (example: /v1/a_bit_of_everything)
|
||||
Template string
|
||||
// OpCodes is a sequence of operations.
|
||||
OpCodes []int
|
||||
// Pool is a constant pool
|
||||
Pool []string
|
||||
// Fields is a list of field paths bound in this template.
|
||||
Fields []string
|
||||
// Version is the version number of the format.
|
||||
Version int
|
||||
}
|
||||
|
||||
// Compiler compiles utilities representation of path templates into marshallable operations.
|
||||
// They can be unmarshalled by runtime.NewPattern.
|
||||
type Compiler interface {
|
||||
Compile() Template
|
||||
}
|
||||
|
||||
type op struct {
|
||||
|
||||
// str is a string operand of the code.
|
||||
// operand is ignored if str is not empty.
|
||||
str string
|
||||
|
||||
// code is the opcode of the operation
|
||||
code OpCode
|
||||
|
||||
// operand is a numeric operand of the code.
|
||||
operand int
|
||||
}
|
||||
|
||||
func (w wildcard) compile() []op {
|
||||
return []op{
|
||||
{code: OpPush},
|
||||
}
|
||||
}
|
||||
|
||||
func (w deepWildcard) compile() []op {
|
||||
return []op{
|
||||
{code: OpPushM},
|
||||
}
|
||||
}
|
||||
|
||||
func (l literal) compile() []op {
|
||||
return []op{
|
||||
{
|
||||
code: OpLitPush,
|
||||
str: string(l),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (v variable) compile() []op {
|
||||
var ops []op
|
||||
for _, s := range v.segments {
|
||||
ops = append(ops, s.compile()...)
|
||||
}
|
||||
|
||||
ops = append(ops, op{
|
||||
code: OpConcatN,
|
||||
operand: len(v.segments),
|
||||
}, op{
|
||||
code: OpCapture,
|
||||
str: v.path,
|
||||
})
|
||||
|
||||
return ops
|
||||
}
|
||||
|
||||
func (t template) Compile() Template {
|
||||
var rawOps []op
|
||||
for _, s := range t.segments {
|
||||
rawOps = append(rawOps, s.compile()...)
|
||||
}
|
||||
|
||||
var (
|
||||
ops []int
|
||||
pool []string
|
||||
fields []string
|
||||
)
|
||||
|
||||
consts := make(map[string]int)
|
||||
|
||||
for _, op := range rawOps {
|
||||
ops = append(ops, int(op.code))
|
||||
if op.str == "" {
|
||||
ops = append(ops, op.operand)
|
||||
} else {
|
||||
if _, ok := consts[op.str]; !ok {
|
||||
consts[op.str] = len(pool)
|
||||
pool = append(pool, op.str)
|
||||
}
|
||||
ops = append(ops, consts[op.str])
|
||||
}
|
||||
|
||||
if op.code == OpCapture {
|
||||
fields = append(fields, op.str)
|
||||
}
|
||||
}
|
||||
|
||||
return Template{
|
||||
Version: opcodeVersion,
|
||||
OpCodes: ops,
|
||||
Pool: pool,
|
||||
Verb: t.verb,
|
||||
Fields: fields,
|
||||
Template: t.template,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
package util
|
||||
|
||||
// download from
|
||||
// https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/parse.go
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
log "go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
// InvalidTemplateError indicates that the path template is not valid.
|
||||
type InvalidTemplateError struct {
|
||||
tmpl string
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e InvalidTemplateError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.msg, e.tmpl)
|
||||
}
|
||||
|
||||
// Parse parses the string representation of path template.
|
||||
func Parse(tmpl string) (Compiler, error) {
|
||||
if !strings.HasPrefix(tmpl, "/") {
|
||||
return template{}, InvalidTemplateError{tmpl: tmpl, msg: "no leading /"}
|
||||
}
|
||||
|
||||
tokens, verb := tokenize(tmpl[1:])
|
||||
p := parser{tokens: tokens}
|
||||
|
||||
segs, err := p.topLevelSegments()
|
||||
if err != nil {
|
||||
return template{}, InvalidTemplateError{tmpl: tmpl, msg: err.Error()}
|
||||
}
|
||||
|
||||
return template{
|
||||
segments: segs,
|
||||
verb: verb,
|
||||
template: tmpl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func tokenize(path string) (tokens []string, verb string) {
|
||||
if path == "" {
|
||||
return []string{eof}, ""
|
||||
}
|
||||
|
||||
const (
|
||||
init = iota
|
||||
field
|
||||
nested
|
||||
)
|
||||
var (
|
||||
st = init
|
||||
)
|
||||
|
||||
for path != "" {
|
||||
var idx int
|
||||
|
||||
switch st {
|
||||
case init:
|
||||
idx = strings.IndexAny(path, "/{")
|
||||
case field:
|
||||
idx = strings.IndexAny(path, ".=}")
|
||||
case nested:
|
||||
idx = strings.IndexAny(path, "/}")
|
||||
}
|
||||
|
||||
if idx < 0 {
|
||||
tokens = append(tokens, path)
|
||||
break
|
||||
}
|
||||
|
||||
switch r := path[idx]; r {
|
||||
case '/', '.':
|
||||
case '{':
|
||||
st = field
|
||||
case '=':
|
||||
st = nested
|
||||
case '}':
|
||||
st = init
|
||||
}
|
||||
|
||||
if idx == 0 {
|
||||
tokens = append(tokens, path[idx:idx+1])
|
||||
} else {
|
||||
tokens = append(tokens, path[:idx], path[idx:idx+1])
|
||||
}
|
||||
|
||||
path = path[idx+1:]
|
||||
}
|
||||
|
||||
l := len(tokens)
|
||||
t := tokens[l-1]
|
||||
|
||||
if idx := strings.LastIndex(t, ":"); idx == 0 {
|
||||
tokens, verb = tokens[:l-1], t[1:]
|
||||
} else if idx > 0 {
|
||||
tokens[l-1], verb = t[:idx], t[idx+1:]
|
||||
}
|
||||
|
||||
tokens = append(tokens, eof)
|
||||
|
||||
return tokens, verb
|
||||
}
|
||||
|
||||
// parser is a parser of the template syntax defined in github.com/googleapis/googleapis/google/api/http.proto.
|
||||
type parser struct {
|
||||
logger log.Logger
|
||||
tokens []string
|
||||
accepted []string
|
||||
}
|
||||
|
||||
// topLevelSegments is the target of this parser.
|
||||
func (p *parser) topLevelSegments() ([]segment, error) {
|
||||
logger := log.LoggerOrDefault(p.logger)
|
||||
|
||||
logger.Logf(log.DebugLevel, "Parsing %q", p.tokens)
|
||||
|
||||
segs, err := p.segments()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Logf(log.DebugLevel, "accept segments: %q; %q", p.accepted, p.tokens)
|
||||
|
||||
if _, err := p.accept(typeEOF); err != nil {
|
||||
return nil, fmt.Errorf("unexpected token %q after segments %q", p.tokens[0], strings.Join(p.accepted, ""))
|
||||
}
|
||||
|
||||
logger.Logf(log.DebugLevel, "accept eof: %q; %q", p.accepted, p.tokens)
|
||||
|
||||
return segs, nil
|
||||
}
|
||||
|
||||
func (p *parser) segments() ([]segment, error) {
|
||||
logger := log.LoggerOrDefault(p.logger)
|
||||
s, err := p.segment()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Logf(log.DebugLevel, "accept segment: %q; %q", p.accepted, p.tokens)
|
||||
|
||||
segs := []segment{s}
|
||||
for {
|
||||
if _, err := p.accept("/"); err != nil {
|
||||
return segs, nil
|
||||
}
|
||||
|
||||
s, err := p.segment()
|
||||
if err != nil {
|
||||
return segs, err
|
||||
}
|
||||
|
||||
segs = append(segs, s)
|
||||
|
||||
logger.Logf(log.DebugLevel, "accept segment: %q; %q", p.accepted, p.tokens)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) segment() (segment, error) {
|
||||
if _, err := p.accept("*"); err == nil {
|
||||
return wildcard{}, nil
|
||||
}
|
||||
|
||||
if _, err := p.accept("**"); err == nil {
|
||||
return deepWildcard{}, nil
|
||||
}
|
||||
|
||||
if l, err := p.literal(); err == nil {
|
||||
return l, nil
|
||||
}
|
||||
|
||||
v, err := p.variable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("segment neither wildcards, literal or variable: %w", err)
|
||||
}
|
||||
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (p *parser) literal() (segment, error) {
|
||||
lit, err := p.accept(typeLiteral)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return literal(lit), nil
|
||||
}
|
||||
|
||||
func (p *parser) variable() (segment, error) {
|
||||
if _, err := p.accept("{"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path, err := p.fieldPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var segs []segment
|
||||
if _, err := p.accept("="); err == nil {
|
||||
segs, err = p.segments()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid segment in variable %q: %w", path, err)
|
||||
}
|
||||
} else {
|
||||
segs = []segment{wildcard{}}
|
||||
}
|
||||
|
||||
if _, err := p.accept("}"); err != nil {
|
||||
return nil, fmt.Errorf("unterminated variable segment: %s", path)
|
||||
}
|
||||
|
||||
return variable{
|
||||
path: path,
|
||||
segments: segs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *parser) fieldPath() (string, error) {
|
||||
c, err := p.accept(typeIdent)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
components := []string{c}
|
||||
|
||||
for {
|
||||
if _, err = p.accept("."); err != nil {
|
||||
return strings.Join(components, "."), nil
|
||||
}
|
||||
c, err := p.accept(typeIdent)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid field path component: %v", err)
|
||||
}
|
||||
components = append(components, c)
|
||||
}
|
||||
}
|
||||
|
||||
// A termType is a type of terminal symbols.
|
||||
type termType string
|
||||
|
||||
// These constants define some of valid values of termType.
|
||||
// They improve readability of parse functions.
|
||||
//
|
||||
// You can also use "/", "*", "**", "." or "=" as valid values.
|
||||
const (
|
||||
typeIdent = termType("ident")
|
||||
typeLiteral = termType("literal")
|
||||
typeEOF = termType("$")
|
||||
)
|
||||
|
||||
const (
|
||||
// eof is the terminal symbol which always appears at the end of token sequence.
|
||||
eof = "\u0000"
|
||||
)
|
||||
|
||||
// accept tries to accept a token in "p".
|
||||
// This function consumes a token and returns it if it matches to the specified "term".
|
||||
// If it doesn't match, the function does not consume any tokens and return an error.
|
||||
func (p *parser) accept(term termType) (string, error) {
|
||||
t := p.tokens[0]
|
||||
|
||||
switch term {
|
||||
case "/", "*", "**", ".", "=", "{", "}":
|
||||
if t != string(term) && t != "/" {
|
||||
return "", fmt.Errorf("expected %q but got %q", term, t)
|
||||
}
|
||||
case typeEOF:
|
||||
if t != eof {
|
||||
return "", fmt.Errorf("expected EOF but got %q", t)
|
||||
}
|
||||
case typeIdent:
|
||||
if err := expectIdent(t); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case typeLiteral:
|
||||
if err := expectPChars(t); err != nil {
|
||||
return "", err
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("unknown termType %q", term)
|
||||
}
|
||||
|
||||
p.tokens = p.tokens[1:]
|
||||
p.accepted = append(p.accepted, t)
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// expectPChars determines if "t" consists of only pchars defined in RFC3986.
|
||||
//
|
||||
// https://www.ietf.org/rfc/rfc3986.txt, P.49
|
||||
//
|
||||
// pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
|
||||
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
|
||||
// / "*" / "+" / "," / ";" / "="
|
||||
// pct-encoded = "%" HEXDIG HEXDIG
|
||||
func expectPChars(t string) error {
|
||||
const (
|
||||
init = iota
|
||||
pct1
|
||||
pct2
|
||||
)
|
||||
|
||||
st := init
|
||||
for _, r := range t {
|
||||
if st != init {
|
||||
if !isHexDigit(r) {
|
||||
return fmt.Errorf("invalid hexdigit: %c(%U)", r, r)
|
||||
}
|
||||
switch st {
|
||||
case pct1:
|
||||
st = pct2
|
||||
case pct2:
|
||||
st = init
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// unreserved
|
||||
switch {
|
||||
case 'A' <= r && r <= 'Z':
|
||||
continue
|
||||
case 'a' <= r && r <= 'z':
|
||||
continue
|
||||
case '0' <= r && r <= '9':
|
||||
continue
|
||||
}
|
||||
switch r {
|
||||
case '-', '.', '_', '~':
|
||||
// unreserved
|
||||
case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=':
|
||||
// sub-delims
|
||||
case ':', '@':
|
||||
// rest of pchar
|
||||
case '%':
|
||||
// pct-encoded
|
||||
st = pct1
|
||||
default:
|
||||
return fmt.Errorf("invalid character in path segment: %q(%U)", r, r)
|
||||
}
|
||||
}
|
||||
|
||||
if st != init {
|
||||
return fmt.Errorf("invalid percent-encoding in %q", t)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// expectIdent determines if "ident" is a valid identifier in .proto schema ([[:alpha:]_][[:alphanum:]_]*).
|
||||
func expectIdent(ident string) error {
|
||||
if ident == "" {
|
||||
return fmt.Errorf("empty identifier")
|
||||
}
|
||||
|
||||
for pos, r := range ident {
|
||||
switch {
|
||||
case '0' <= r && r <= '9':
|
||||
if pos == 0 {
|
||||
return fmt.Errorf("identifier starting with digit: %s", ident)
|
||||
}
|
||||
|
||||
continue
|
||||
case 'A' <= r && r <= 'Z':
|
||||
continue
|
||||
case 'a' <= r && r <= 'z':
|
||||
continue
|
||||
case r == '_':
|
||||
continue
|
||||
default:
|
||||
return fmt.Errorf("invalid character %q(%U) in identifier: %s", r, r, ident)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isHexDigit(r rune) bool {
|
||||
switch {
|
||||
case '0' <= r && r <= '9':
|
||||
return true
|
||||
case 'A' <= r && r <= 'F':
|
||||
return true
|
||||
case 'a' <= r && r <= 'f':
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package util
|
||||
|
||||
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/utilities/pattern.go
|
||||
|
||||
// An OpCode is a opcode of compiled path patterns.
|
||||
type OpCode int
|
||||
|
||||
// These constants are the valid values of OpCode.
|
||||
const (
|
||||
// OpNop does nothing.
|
||||
OpNop = OpCode(iota)
|
||||
// OpPush pushes a component to stack.
|
||||
OpPush
|
||||
// OpLitPush pushes a component to stack if it matches to the literal.
|
||||
OpLitPush
|
||||
// OpPushM concatenates the remaining components and pushes it to stack.
|
||||
OpPushM
|
||||
// OpConcatN pops N items from stack, concatenates them and pushes it back to stack.
|
||||
OpConcatN
|
||||
// OpCapture pops an item and binds it to the variable.
|
||||
OpCapture
|
||||
// OpEnd is the least positive invalid opcode.
|
||||
OpEnd
|
||||
)
|
||||
@@ -0,0 +1,282 @@
|
||||
package util
|
||||
|
||||
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/runtime/pattern.go
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
log "go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotMatch indicates that the given HTTP request path does not match to the pattern.
|
||||
ErrNotMatch = errors.New("not match to the path pattern")
|
||||
// ErrInvalidPattern indicates that the given definition of Pattern is not valid.
|
||||
ErrInvalidPattern = errors.New("invalid pattern")
|
||||
)
|
||||
|
||||
type rop struct {
|
||||
code OpCode
|
||||
operand int
|
||||
}
|
||||
|
||||
// Pattern is a template pattern of http request paths defined in github.com/googleapis/googleapis/google/api/http.proto.
|
||||
type Pattern struct {
|
||||
// verb is the VERB part of the path pattern. It is empty if the pattern does not have VERB part.
|
||||
verb string
|
||||
// ops is a list of operations
|
||||
ops []rop
|
||||
// pool is a constant pool indexed by the operands or vars.
|
||||
pool []string
|
||||
// vars is a list of variables names to be bound by this pattern
|
||||
vars []string
|
||||
// stacksize is the max depth of the stack
|
||||
stacksize int
|
||||
// tailLen is the length of the fixed-size segments after a deep wildcard
|
||||
tailLen int
|
||||
// assumeColonVerb indicates whether a path suffix after a final
|
||||
// colon may only be interpreted as a verb.
|
||||
assumeColonVerb bool
|
||||
}
|
||||
|
||||
type patternOptions struct {
|
||||
logger log.Logger
|
||||
assumeColonVerb bool
|
||||
}
|
||||
|
||||
// PatternOpt is an option for creating Patterns.
|
||||
type PatternOpt func(*patternOptions)
|
||||
|
||||
// PatternLogger sets the logger.
|
||||
func PatternLogger(l log.Logger) PatternOpt {
|
||||
return func(po *patternOptions) {
|
||||
po.logger = l
|
||||
}
|
||||
}
|
||||
|
||||
// NewPattern returns a new Pattern from the given definition values.
|
||||
// "ops" is a sequence of op codes. "pool" is a constant pool.
|
||||
// "verb" is the verb part of the pattern. It is empty if the pattern does not have the part.
|
||||
// "version" must be 1 for now.
|
||||
// It returns an error if the given definition is invalid.
|
||||
func NewPattern(version int, ops []int, pool []string, verb string, opts ...PatternOpt) (Pattern, error) {
|
||||
options := patternOptions{
|
||||
assumeColonVerb: true,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
logger := log.LoggerOrDefault(options.logger)
|
||||
|
||||
if version != 1 {
|
||||
logger.Logf(log.DebugLevel, "unsupported version: %d", version)
|
||||
return Pattern{}, ErrInvalidPattern
|
||||
}
|
||||
|
||||
l := len(ops)
|
||||
if l%2 != 0 {
|
||||
logger.Logf(log.DebugLevel, "odd number of ops codes: %d", l)
|
||||
return Pattern{}, ErrInvalidPattern
|
||||
}
|
||||
|
||||
var (
|
||||
typedOps []rop
|
||||
stack, maxstack int
|
||||
tailLen int
|
||||
pushMSeen bool
|
||||
vars []string
|
||||
)
|
||||
|
||||
for i := 0; i < l; i += 2 {
|
||||
op := rop{code: OpCode(ops[i]), operand: ops[i+1]}
|
||||
|
||||
switch op.code {
|
||||
case OpNop:
|
||||
continue
|
||||
case OpPush:
|
||||
if pushMSeen {
|
||||
tailLen++
|
||||
}
|
||||
stack++
|
||||
case OpPushM:
|
||||
if pushMSeen {
|
||||
logger.Logf(log.DebugLevel, "pushM appears twice")
|
||||
return Pattern{}, ErrInvalidPattern
|
||||
}
|
||||
|
||||
pushMSeen = true
|
||||
stack++
|
||||
case OpLitPush:
|
||||
if op.operand < 0 || len(pool) <= op.operand {
|
||||
logger.Logf(log.DebugLevel, "negative literal index: %d", op.operand)
|
||||
return Pattern{}, ErrInvalidPattern
|
||||
}
|
||||
|
||||
if pushMSeen {
|
||||
tailLen++
|
||||
}
|
||||
stack++
|
||||
case OpConcatN:
|
||||
if op.operand <= 0 {
|
||||
logger.Logf(log.DebugLevel, "negative concat size: %d", op.operand)
|
||||
return Pattern{}, ErrInvalidPattern
|
||||
}
|
||||
|
||||
stack -= op.operand
|
||||
if stack < 0 {
|
||||
logger.Logf(log.DebugLevel, "stack underflow")
|
||||
return Pattern{}, ErrInvalidPattern
|
||||
}
|
||||
stack++
|
||||
case OpCapture:
|
||||
if op.operand < 0 || len(pool) <= op.operand {
|
||||
logger.Logf(log.DebugLevel, "variable name index out of bound: %d", op.operand)
|
||||
return Pattern{}, ErrInvalidPattern
|
||||
}
|
||||
|
||||
v := pool[op.operand]
|
||||
op.operand = len(vars)
|
||||
vars = append(vars, v)
|
||||
stack--
|
||||
|
||||
if stack < 0 {
|
||||
logger.Logf(log.DebugLevel, "stack underflow")
|
||||
return Pattern{}, ErrInvalidPattern
|
||||
}
|
||||
default:
|
||||
logger.Logf(log.DebugLevel, "invalid opcode: %d", op.code)
|
||||
return Pattern{}, ErrInvalidPattern
|
||||
}
|
||||
|
||||
if maxstack < stack {
|
||||
maxstack = stack
|
||||
}
|
||||
|
||||
typedOps = append(typedOps, op)
|
||||
}
|
||||
|
||||
return Pattern{
|
||||
ops: typedOps,
|
||||
pool: pool,
|
||||
vars: vars,
|
||||
stacksize: maxstack,
|
||||
tailLen: tailLen,
|
||||
verb: verb,
|
||||
assumeColonVerb: options.assumeColonVerb,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MustPattern is a helper function which makes it easier to call NewPattern in variable initialization.
|
||||
func MustPattern(p Pattern, err error) Pattern {
|
||||
if err != nil {
|
||||
log.Logf(log.FatalLevel, "Pattern initialization failed: %v", err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// Match examines components if it matches to the Pattern.
|
||||
// If it matches, the function returns a mapping from field paths to their captured values.
|
||||
// If otherwise, the function returns an error.
|
||||
func (p Pattern) Match(components []string, verb string) (map[string]string, error) {
|
||||
if p.verb != verb {
|
||||
if p.assumeColonVerb || p.verb != "" {
|
||||
return nil, ErrNotMatch
|
||||
}
|
||||
if len(components) == 0 {
|
||||
components = []string{":" + verb}
|
||||
} else {
|
||||
components = append([]string{}, components...)
|
||||
components[len(components)-1] += ":" + verb
|
||||
}
|
||||
verb = ""
|
||||
}
|
||||
|
||||
var pos int
|
||||
stack := make([]string, 0, p.stacksize)
|
||||
captured := make([]string, len(p.vars))
|
||||
l := len(components)
|
||||
for _, op := range p.ops {
|
||||
switch op.code {
|
||||
case OpNop:
|
||||
continue
|
||||
case OpPush, OpLitPush:
|
||||
if pos >= l {
|
||||
return nil, ErrNotMatch
|
||||
}
|
||||
c := components[pos]
|
||||
if op.code == OpLitPush {
|
||||
if lit := p.pool[op.operand]; c != lit {
|
||||
return nil, ErrNotMatch
|
||||
}
|
||||
}
|
||||
stack = append(stack, c)
|
||||
pos++
|
||||
case OpPushM:
|
||||
end := len(components)
|
||||
if end < pos+p.tailLen {
|
||||
return nil, ErrNotMatch
|
||||
}
|
||||
end -= p.tailLen
|
||||
stack = append(stack, strings.Join(components[pos:end], "/"))
|
||||
pos = end
|
||||
case OpConcatN:
|
||||
n := op.operand
|
||||
l := len(stack) - n
|
||||
stack = append(stack[:l], strings.Join(stack[l:], "/"))
|
||||
case OpCapture:
|
||||
n := len(stack) - 1
|
||||
captured[op.operand] = stack[n]
|
||||
stack = stack[:n]
|
||||
}
|
||||
}
|
||||
if pos < l {
|
||||
return nil, ErrNotMatch
|
||||
}
|
||||
bindings := make(map[string]string)
|
||||
for i, val := range captured {
|
||||
bindings[p.vars[i]] = val
|
||||
}
|
||||
return bindings, nil
|
||||
}
|
||||
|
||||
// Verb returns the verb part of the Pattern.
|
||||
func (p Pattern) Verb() string { return p.verb }
|
||||
|
||||
func (p Pattern) String() string {
|
||||
var stack []string
|
||||
for _, op := range p.ops {
|
||||
switch op.code {
|
||||
case OpNop:
|
||||
continue
|
||||
case OpPush:
|
||||
stack = append(stack, "*")
|
||||
case OpLitPush:
|
||||
stack = append(stack, p.pool[op.operand])
|
||||
case OpPushM:
|
||||
stack = append(stack, "**")
|
||||
case OpConcatN:
|
||||
n := op.operand
|
||||
l := len(stack) - n
|
||||
stack = append(stack[:l], strings.Join(stack[l:], "/"))
|
||||
case OpCapture:
|
||||
n := len(stack) - 1
|
||||
stack[n] = fmt.Sprintf("{%s=%s}", p.vars[op.operand], stack[n])
|
||||
}
|
||||
}
|
||||
segs := strings.Join(stack, "/")
|
||||
if p.verb != "" {
|
||||
return fmt.Sprintf("/%s:%s", segs, p.verb)
|
||||
}
|
||||
return "/" + segs
|
||||
}
|
||||
|
||||
// AssumeColonVerbOpt indicates whether a path suffix after a final
|
||||
// colon may only be interpreted as a verb.
|
||||
func AssumeColonVerbOpt(val bool) PatternOpt {
|
||||
return PatternOpt(func(o *patternOptions) {
|
||||
o.assumeColonVerb = val
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package util
|
||||
|
||||
// download from
|
||||
// https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/types.go
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type template struct {
|
||||
verb string
|
||||
template string
|
||||
segments []segment
|
||||
}
|
||||
|
||||
type segment interface {
|
||||
fmt.Stringer
|
||||
compile() (ops []op)
|
||||
}
|
||||
|
||||
type wildcard struct{}
|
||||
|
||||
type deepWildcard struct{}
|
||||
|
||||
type literal string
|
||||
|
||||
type variable struct {
|
||||
path string
|
||||
segments []segment
|
||||
}
|
||||
|
||||
func (wildcard) String() string {
|
||||
return "*"
|
||||
}
|
||||
|
||||
func (deepWildcard) String() string {
|
||||
return "**"
|
||||
}
|
||||
|
||||
func (l literal) String() string {
|
||||
return string(l)
|
||||
}
|
||||
|
||||
func (v variable) String() string {
|
||||
var segs []string
|
||||
for _, s := range v.segments {
|
||||
segs = append(segs, s.String())
|
||||
}
|
||||
|
||||
return fmt.Sprintf("{%s=%s}", v.path, strings.Join(segs, "/"))
|
||||
}
|
||||
|
||||
func (t template) String() string {
|
||||
var segs []string
|
||||
for _, s := range t.segments {
|
||||
segs = append(segs, s.String())
|
||||
}
|
||||
|
||||
str := strings.Join(segs, "/")
|
||||
if t.verb != "" {
|
||||
str = fmt.Sprintf("%s:%s", str, t.verb)
|
||||
}
|
||||
|
||||
return "/" + str
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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"
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
// 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
|
||||
// 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{}
|
||||
|
||||
// Logger is the underling logging framework
|
||||
Logger logger.Logger
|
||||
// CA is the CA to use
|
||||
CA string
|
||||
// AcceptTLS must be set to true to indicate that you have read your
|
||||
// provider's terms of service.
|
||||
AcceptToS bool
|
||||
// Issue certificates for domains on demand. Otherwise, certs will be
|
||||
// retrieved / issued on start-up.
|
||||
OnDemand bool
|
||||
}
|
||||
|
||||
// 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
|
||||
// implementation.
|
||||
func Cache(c interface{}) Option {
|
||||
return func(o *Options) {
|
||||
o.Cache = c
|
||||
}
|
||||
}
|
||||
|
||||
// Logger sets the underline logger.
|
||||
func Logger(l logger.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = l
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultOptions uses the Let's Encrypt Production CA, with DNS Challenge disabled.
|
||||
func DefaultOptions() Options {
|
||||
return Options{
|
||||
AcceptToS: true,
|
||||
CA: LetsEncryptProductionCA,
|
||||
OnDemand: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cors
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
AllowOrigin string
|
||||
AllowMethods string
|
||||
AllowHeaders string
|
||||
AllowCredentials bool
|
||||
}
|
||||
|
||||
// CombinedCORSHandler wraps a server and provides CORS headers.
|
||||
func CombinedCORSHandler(h http.Handler, config *Config) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if config != nil {
|
||||
SetHeaders(w, r, config)
|
||||
}
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// SetHeaders sets the CORS headers.
|
||||
func SetHeaders(w http.ResponseWriter, _ *http.Request, config *Config) {
|
||||
set := func(w http.ResponseWriter, k, v string) {
|
||||
if v := w.Header().Get(k); len(v) > 0 {
|
||||
return
|
||||
}
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
// For forward-compatible code, default values may not be provided in the future
|
||||
if config.AllowCredentials {
|
||||
set(w, "Access-Control-Allow-Credentials", "true")
|
||||
} else {
|
||||
set(w, "Access-Control-Allow-Credentials", "false")
|
||||
}
|
||||
if config.AllowOrigin == "" {
|
||||
set(w, "Access-Control-Allow-Origin", "*")
|
||||
} else {
|
||||
set(w, "Access-Control-Allow-Origin", config.AllowOrigin)
|
||||
}
|
||||
if config.AllowMethods == "" {
|
||||
set(w, "Access-Control-Allow-Methods", "POST, PATCH, GET, OPTIONS, PUT, DELETE")
|
||||
} else {
|
||||
set(w, "Access-Control-Allow-Methods", config.AllowMethods)
|
||||
}
|
||||
if config.AllowHeaders == "" {
|
||||
set(w, "Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
||||
} else {
|
||||
set(w, "Access-Control-Allow-Headers", config.AllowHeaders)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// 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"
|
||||
"go-micro.dev/v4/api/server"
|
||||
"go-micro.dev/v4/api/server/cors"
|
||||
log "go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
type httpServer struct {
|
||||
opts server.Options
|
||||
|
||||
mux *http.ServeMux
|
||||
exit chan chan error
|
||||
address string
|
||||
|
||||
mtx sync.RWMutex
|
||||
}
|
||||
|
||||
func NewServer(address string, opts ...server.Option) server.Server {
|
||||
options := server.NewOptions(opts...)
|
||||
|
||||
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, s.opts.CORSConfig)
|
||||
}
|
||||
|
||||
// wrap with logger
|
||||
handler = handlers.CombinedLoggingHandler(os.Stdout, handler)
|
||||
|
||||
s.mux.Handle(path, handler)
|
||||
}
|
||||
|
||||
func (s *httpServer) Start() error {
|
||||
logger := s.opts.Logger
|
||||
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
|
||||
}
|
||||
|
||||
logger.Logf(log.InfoLevel, "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.Log(log.FatalLevel, err)
|
||||
logger.Log(log.ErrorLevel, 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"
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
|
||||
"go-micro.dev/v4/api/resolver"
|
||||
"go-micro.dev/v4/api/server/acme"
|
||||
"go-micro.dev/v4/api/server/cors"
|
||||
"go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
type Options struct {
|
||||
ACMEProvider acme.Provider
|
||||
Resolver resolver.Resolver
|
||||
Logger logger.Logger
|
||||
CORSConfig *cors.Config
|
||||
TLSConfig *tls.Config
|
||||
ACMEHosts []string
|
||||
Wrappers []Wrapper
|
||||
EnableACME bool
|
||||
EnableCORS bool
|
||||
EnableTLS bool
|
||||
}
|
||||
|
||||
type Wrapper func(h http.Handler) http.Handler
|
||||
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
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 CORSConfig(c *cors.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.CORSConfig = c
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Logger sets the underline logging framework.
|
||||
func Logger(l logger.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = l
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// 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
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
// Package auth provides authentication and authorization capability
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// BearerScheme used for Authorization header.
|
||||
BearerScheme = "Bearer "
|
||||
// ScopePublic is the scope applied to a rule to allow access to the public.
|
||||
ScopePublic = ""
|
||||
// ScopeAccount is the scope applied to a rule to limit to users with any valid account.
|
||||
ScopeAccount = "*"
|
||||
)
|
||||
|
||||
var (
|
||||
// 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.
|
||||
ErrForbidden = errors.New("resource forbidden")
|
||||
)
|
||||
|
||||
// Auth provides authentication and authorization.
|
||||
type Auth interface {
|
||||
// Init the auth
|
||||
Init(opts ...Option)
|
||||
// Options set for auth
|
||||
Options() Options
|
||||
// Generate a new account
|
||||
Generate(id string, opts ...GenerateOption) (*Account, error)
|
||||
// Inspect a token
|
||||
Inspect(token string) (*Account, error)
|
||||
// Token generated using refresh token or credentials
|
||||
Token(opts ...TokenOption) (*Token, error)
|
||||
// String returns the name of the implementation
|
||||
String() string
|
||||
}
|
||||
|
||||
// Rules manages access to resources.
|
||||
type Rules interface {
|
||||
// Verify an account has access to a resource using the rules
|
||||
Verify(acc *Account, res *Resource, opts ...VerifyOption) error
|
||||
// Grant access to a resource
|
||||
Grant(rule *Rule) error
|
||||
// Revoke access to a resource
|
||||
Revoke(rule *Rule) error
|
||||
// List returns all the rules used to verify requests
|
||||
List(...ListOption) ([]*Rule, error)
|
||||
}
|
||||
|
||||
// Account provided by an auth provider.
|
||||
type Account struct {
|
||||
// Any other associated metadata
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
// ID of the account e.g. email
|
||||
ID string `json:"id"`
|
||||
// Type of the account, e.g. service
|
||||
Type string `json:"type"`
|
||||
// Issuer of the account
|
||||
Issuer string `json:"issuer"`
|
||||
// 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 {
|
||||
// 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.
|
||||
func (t *Token) Expired() bool {
|
||||
return t.Expiry.Unix() < time.Now().Unix()
|
||||
}
|
||||
|
||||
// Resource is an entity such as a user or.
|
||||
type Resource struct {
|
||||
// Name of the resource, e.g. go.micro.service.notes
|
||||
Name string `json:"name"`
|
||||
// Type of resource, e.g. service
|
||||
Type string `json:"type"`
|
||||
// Endpoint resource e.g NotesService.Create
|
||||
Endpoint string `json:"endpoint"`
|
||||
}
|
||||
|
||||
// Access defines the type of access a rule grants.
|
||||
type Access int
|
||||
|
||||
const (
|
||||
// AccessGranted to a resource.
|
||||
AccessGranted Access = iota
|
||||
// AccessDenied to a resource.
|
||||
AccessDenied
|
||||
)
|
||||
|
||||
// Rule is used to verify access to a resource.
|
||||
type Rule struct {
|
||||
// Resource the rule applies to
|
||||
Resource *Resource
|
||||
// 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
|
||||
// Access determines if the rule grants or denies access to the resource
|
||||
Access Access
|
||||
// Priority the rule should take when verifying a request, the higher the value the sooner the
|
||||
// rule will be applied
|
||||
Priority int32
|
||||
}
|
||||
|
||||
type accountKey struct{}
|
||||
|
||||
// AccountFromContext gets the account from the context, which
|
||||
// is set by the auth wrapper at the start of a call. If the account
|
||||
// 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) {
|
||||
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 {
|
||||
return context.WithValue(ctx, accountKey{}, account)
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultAuth = NewAuth()
|
||||
)
|
||||
|
||||
func NewAuth(opts ...Option) Auth {
|
||||
options := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return &noop{
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
|
||||
func NewRules() Rules {
|
||||
return new(noopRules)
|
||||
}
|
||||
|
||||
type noop struct {
|
||||
opts Options
|
||||
}
|
||||
|
||||
type noopRules struct{}
|
||||
|
||||
// String returns the name of the implementation.
|
||||
func (n *noop) String() string {
|
||||
return "noop"
|
||||
}
|
||||
|
||||
// Init the auth.
|
||||
func (n *noop) Init(opts ...Option) {
|
||||
for _, o := range opts {
|
||||
o(&n.opts)
|
||||
}
|
||||
}
|
||||
|
||||
// Options set for auth.
|
||||
func (n *noop) Options() Options {
|
||||
return n.opts
|
||||
}
|
||||
|
||||
// Generate a new account.
|
||||
func (n *noop) 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().Namespace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Grant access to a resource.
|
||||
func (n *noopRules) Grant(rule *Rule) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Revoke access to a resource.
|
||||
func (n *noopRules) Revoke(rule *Rule) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rules used to verify requests
|
||||
// Verify an account has access to a resource.
|
||||
func (n *noopRules) Verify(acc *Account, res *Resource, opts ...VerifyOption) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *noopRules) List(opts ...ListOption) ([]*Rule, error) {
|
||||
return []*Rule{}, nil
|
||||
}
|
||||
|
||||
// Inspect a token.
|
||||
func (n *noop) Inspect(token string) (*Account, error) {
|
||||
return &Account{ID: uuid.New().String(), Issuer: n.Options().Namespace}, nil
|
||||
}
|
||||
|
||||
// Token generation using an account id and secret.
|
||||
func (n *noop) Token(opts ...TokenOption) (*Token, error) {
|
||||
return &Token{}, nil
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
// Logger is the underline logger
|
||||
Logger logger.Logger
|
||||
// Token is the services token used to authenticate itself
|
||||
Token *Token
|
||||
// Namespace the service belongs to
|
||||
Namespace string
|
||||
// ID is the services auth ID
|
||||
ID string
|
||||
// Secret is used to authenticate the service
|
||||
Secret string
|
||||
// PublicKey for decoding JWTs
|
||||
PublicKey string
|
||||
// PrivateKey for encoding JWTs
|
||||
PrivateKey string
|
||||
// Addrs sets the addresses of auth
|
||||
Addrs []string
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
// Addrs is the auth addresses to use.
|
||||
func Addrs(addrs ...string) Option {
|
||||
return func(o *Options) {
|
||||
o.Addrs = addrs
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace the service belongs to.
|
||||
func Namespace(n string) Option {
|
||||
return func(o *Options) {
|
||||
o.Namespace = n
|
||||
}
|
||||
}
|
||||
|
||||
// PublicKey is the JWT public key.
|
||||
func PublicKey(key string) Option {
|
||||
return func(o *Options) {
|
||||
o.PublicKey = key
|
||||
}
|
||||
}
|
||||
|
||||
// PrivateKey is the JWT private key.
|
||||
func PrivateKey(key string) Option {
|
||||
return func(o *Options) {
|
||||
o.PrivateKey = key
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger sets the underline logger.
|
||||
func WithLogger(l logger.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = l
|
||||
}
|
||||
}
|
||||
|
||||
// Credentials sets the auth credentials.
|
||||
func Credentials(id, secret string) Option {
|
||||
return func(o *Options) {
|
||||
o.ID = id
|
||||
o.Secret = secret
|
||||
}
|
||||
}
|
||||
|
||||
// ClientToken sets the auth token to use when making requests.
|
||||
func ClientToken(token *Token) Option {
|
||||
return func(o *Options) {
|
||||
o.Token = token
|
||||
}
|
||||
}
|
||||
|
||||
type GenerateOptions struct {
|
||||
// Metadata associated with the account
|
||||
Metadata map[string]string
|
||||
// Provider of the account, e.g. oauth
|
||||
Provider string
|
||||
// Type of the account, e.g. user
|
||||
Type string
|
||||
// Secret used to authenticate the account
|
||||
Secret string
|
||||
// Scopes the account has access too
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
type GenerateOption func(o *GenerateOptions)
|
||||
|
||||
// WithSecret for the generated account.
|
||||
func WithSecret(s string) GenerateOption {
|
||||
return func(o *GenerateOptions) {
|
||||
o.Secret = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithType for the generated account.
|
||||
func WithType(t string) GenerateOption {
|
||||
return func(o *GenerateOptions) {
|
||||
o.Type = t
|
||||
}
|
||||
}
|
||||
|
||||
// WithMetadata for the generated account.
|
||||
func WithMetadata(md map[string]string) GenerateOption {
|
||||
return func(o *GenerateOptions) {
|
||||
o.Metadata = md
|
||||
}
|
||||
}
|
||||
|
||||
// WithProvider for the generated account.
|
||||
func WithProvider(p string) GenerateOption {
|
||||
return func(o *GenerateOptions) {
|
||||
o.Provider = p
|
||||
}
|
||||
}
|
||||
|
||||
// WithScopes for the generated account.
|
||||
func WithScopes(s ...string) GenerateOption {
|
||||
return func(o *GenerateOptions) {
|
||||
o.Scopes = s
|
||||
}
|
||||
}
|
||||
|
||||
// NewGenerateOptions from a slice of options.
|
||||
func NewGenerateOptions(opts ...GenerateOption) GenerateOptions {
|
||||
var options GenerateOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
type TokenOptions struct {
|
||||
// ID for the account
|
||||
ID string
|
||||
// Secret for the account
|
||||
Secret string
|
||||
// RefreshToken is used to refesh a token
|
||||
RefreshToken string
|
||||
// Expiry is the time the token should live for
|
||||
Expiry time.Duration
|
||||
}
|
||||
|
||||
type TokenOption func(o *TokenOptions)
|
||||
|
||||
// WithExpiry for the token.
|
||||
func WithExpiry(ex time.Duration) TokenOption {
|
||||
return func(o *TokenOptions) {
|
||||
o.Expiry = ex
|
||||
}
|
||||
}
|
||||
|
||||
func WithCredentials(id, secret string) TokenOption {
|
||||
return func(o *TokenOptions) {
|
||||
o.ID = id
|
||||
o.Secret = secret
|
||||
}
|
||||
}
|
||||
|
||||
func WithToken(rt string) TokenOption {
|
||||
return func(o *TokenOptions) {
|
||||
o.RefreshToken = rt
|
||||
}
|
||||
}
|
||||
|
||||
// NewTokenOptions from a slice of options.
|
||||
func NewTokenOptions(opts ...TokenOption) TokenOptions {
|
||||
var options TokenOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
// set default expiry of token
|
||||
if options.Expiry == 0 {
|
||||
options.Expiry = time.Minute
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
type VerifyOptions struct {
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
type VerifyOption func(o *VerifyOptions)
|
||||
|
||||
func VerifyContext(ctx context.Context) VerifyOption {
|
||||
return func(o *VerifyOptions) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
type ListOptions struct {
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
type ListOption func(o *ListOptions)
|
||||
|
||||
func RulesContext(ctx context.Context) ListOption {
|
||||
return func(o *ListOptions) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Verify 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.
|
||||
func Verify(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}
|
||||
|
||||
// the rule is only to be applied if the name matches the resource or is catch-all (*)
|
||||
validNames := []string{"*", res.Name}
|
||||
|
||||
// rules can have wildcard excludes on endpoints since this can also be a path for web services,
|
||||
// e.g. /foo/* would include /foo/bar. We also want to check for wildcards and the exact endpoint
|
||||
validEndpoints := []string{"*", res.Endpoint}
|
||||
if comps := strings.Split(res.Endpoint, "/"); len(comps) > 1 {
|
||||
for i := 1; i < len(comps)+1; i++ {
|
||||
wildcard := fmt.Sprintf("%v/*", strings.Join(comps[0:i], "/"))
|
||||
validEndpoints = append(validEndpoints, wildcard)
|
||||
}
|
||||
}
|
||||
|
||||
// filter the rules to the ones which match the criteria above
|
||||
filteredRules := make([]*Rule, 0)
|
||||
for _, rule := range rules {
|
||||
if !include(validTypes, rule.Resource.Type) {
|
||||
continue
|
||||
}
|
||||
if !include(validNames, rule.Resource.Name) {
|
||||
continue
|
||||
}
|
||||
if !include(validEndpoints, rule.Resource.Endpoint) {
|
||||
continue
|
||||
}
|
||||
filteredRules = append(filteredRules, rule)
|
||||
}
|
||||
|
||||
// sort the filtered rules by priority, highest to lowest
|
||||
sort.SliceStable(filteredRules, func(i, j int) bool {
|
||||
return filteredRules[i].Priority > filteredRules[j].Priority
|
||||
})
|
||||
|
||||
// loop through the rules and check for a rule which applies to this account
|
||||
for _, rule := range filteredRules {
|
||||
// a blank scope indicates the rule applies to everyone, even nil accounts
|
||||
if rule.Scope == ScopePublic && rule.Access == AccessDenied {
|
||||
return ErrForbidden
|
||||
} else if rule.Scope == ScopePublic && rule.Access == AccessGranted {
|
||||
return nil
|
||||
}
|
||||
|
||||
// all further checks require an account
|
||||
if acc == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// this rule applies to any account
|
||||
if rule.Scope == ScopeAccount && rule.Access == AccessDenied {
|
||||
return ErrForbidden
|
||||
} else if rule.Scope == ScopeAccount && rule.Access == AccessGranted {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if the account has the necessary scope
|
||||
if include(acc.Scopes, rule.Scope) && rule.Access == AccessDenied {
|
||||
return ErrForbidden
|
||||
} else if include(acc.Scopes, rule.Scope) && rule.Access == AccessGranted {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// if no rules matched then return forbidden
|
||||
return ErrForbidden
|
||||
}
|
||||
|
||||
// include is a helper function which checks to see if the slice contains the value. includes is
|
||||
// not case sensitive.
|
||||
func include(slice []string, val string) bool {
|
||||
for _, s := range slice {
|
||||
if strings.EqualFold(s, val) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// Package broker is an interface used for asynchronous messaging
|
||||
package broker
|
||||
|
||||
// Broker is an interface used for asynchronous messaging.
|
||||
type Broker interface {
|
||||
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)
|
||||
String() string
|
||||
}
|
||||
|
||||
// Handler is used to process messages via a subscription of a topic.
|
||||
// The handler is passed a publication interface which contains the
|
||||
// message and optional Ack method to acknowledge receipt of the message.
|
||||
type Handler func(Event) error
|
||||
|
||||
// Message is a message send/received from the broker.
|
||||
type Message struct {
|
||||
Header map[string]string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
// Event is given to a subscription handler for processing.
|
||||
type Event interface {
|
||||
Topic() string
|
||||
Message() *Message
|
||||
Ack() error
|
||||
Error() error
|
||||
}
|
||||
|
||||
// Subscriber is a convenience return type for the Subscribe method.
|
||||
type Subscriber interface {
|
||||
Options() SubscribeOptions
|
||||
Topic() string
|
||||
Unsubscribe() error
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultBroker is the default Broker.
|
||||
DefaultBroker = NewBroker()
|
||||
)
|
||||
|
||||
func Init(opts ...Option) error {
|
||||
return DefaultBroker.Init(opts...)
|
||||
}
|
||||
|
||||
func Connect() error {
|
||||
return DefaultBroker.Connect()
|
||||
}
|
||||
|
||||
func Disconnect() error {
|
||||
return DefaultBroker.Disconnect()
|
||||
}
|
||||
|
||||
func Publish(topic string, msg *Message, opts ...PublishOption) error {
|
||||
return DefaultBroker.Publish(topic, msg, opts...)
|
||||
}
|
||||
|
||||
func Subscribe(topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
|
||||
return DefaultBroker.Subscribe(topic, handler, opts...)
|
||||
}
|
||||
|
||||
// String returns the name of the Broker.
|
||||
func String() string {
|
||||
return DefaultBroker.String()
|
||||
}
|
||||
+711
@@ -0,0 +1,711 @@
|
||||
// Package broker provides a http based message broker
|
||||
package broker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go-micro.dev/v4/codec/json"
|
||||
merr "go-micro.dev/v4/errors"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/registry/cache"
|
||||
"go-micro.dev/v4/transport/headers"
|
||||
maddr "go-micro.dev/v4/util/addr"
|
||||
mnet "go-micro.dev/v4/util/net"
|
||||
mls "go-micro.dev/v4/util/tls"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
// HTTP Broker is a point to point async broker.
|
||||
type httpBroker struct {
|
||||
opts Options
|
||||
|
||||
r registry.Registry
|
||||
|
||||
mux *http.ServeMux
|
||||
|
||||
c *http.Client
|
||||
subscribers map[string][]*httpSubscriber
|
||||
exit chan chan error
|
||||
|
||||
inbox map[string][][]byte
|
||||
id string
|
||||
address string
|
||||
|
||||
sync.RWMutex
|
||||
|
||||
// offline message inbox
|
||||
mtx sync.RWMutex
|
||||
running bool
|
||||
}
|
||||
|
||||
type httpSubscriber struct {
|
||||
opts SubscribeOptions
|
||||
fn Handler
|
||||
svc *registry.Service
|
||||
hb *httpBroker
|
||||
id string
|
||||
topic string
|
||||
}
|
||||
|
||||
type httpEvent struct {
|
||||
err error
|
||||
m *Message
|
||||
t string
|
||||
}
|
||||
|
||||
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 ...Option) Broker {
|
||||
options := *NewOptions(opts...)
|
||||
|
||||
options.Registry = registry.DefaultRegistry
|
||||
options.Codec = json.Marshaler{}
|
||||
|
||||
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 *httpEvent) Ack() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpEvent) Error() error {
|
||||
return h.err
|
||||
}
|
||||
|
||||
func (h *httpEvent) Message() *Message {
|
||||
return h.m
|
||||
}
|
||||
|
||||
func (h *httpEvent) Topic() string {
|
||||
return h.t
|
||||
}
|
||||
|
||||
func (h *httpSubscriber) Options() 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 := io.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 m *Message
|
||||
if err = h.opts.Codec.Unmarshal(b, &m); err != nil {
|
||||
errr := merr.InternalServerError("go.micro.broker", "Error parsing request body: %v", err)
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(errr.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
topic := m.Header[headers.Message]
|
||||
// delete(m.Header, ":topic")
|
||||
|
||||
if len(topic) == 0 {
|
||||
errr := merr.InternalServerError("go.micro.broker", "Topic not found")
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(errr.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
p := &httpEvent{m: m, t: topic}
|
||||
id := req.Form.Get("id")
|
||||
|
||||
//nolint:prealloc
|
||||
var subs []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 {
|
||||
p.err = fn(p)
|
||||
}
|
||||
}
|
||||
|
||||
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 = registry.DefaultRegistry
|
||||
}
|
||||
// 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 ...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 = registry.DefaultRegistry
|
||||
}
|
||||
|
||||
// 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() Options {
|
||||
return h.opts
|
||||
}
|
||||
|
||||
func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption) error {
|
||||
// create the message first
|
||||
m := &Message{
|
||||
Header: make(map[string]string),
|
||||
Body: msg.Body,
|
||||
}
|
||||
|
||||
for k, v := range msg.Header {
|
||||
m.Header[k] = v
|
||||
}
|
||||
|
||||
m.Header[headers.Message] = 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(io.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 Handler, opts ...SubscribeOption) (Subscriber, error) {
|
||||
var err error
|
||||
var host, port string
|
||||
options := 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 := ®istry.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 := ®istry.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 ...Option) Broker {
|
||||
return newHttpBroker(opts...)
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
// Package memory provides a memory broker
|
||||
package broker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
log "go-micro.dev/v4/logger"
|
||||
maddr "go-micro.dev/v4/util/addr"
|
||||
mnet "go-micro.dev/v4/util/net"
|
||||
)
|
||||
|
||||
type memoryBroker struct {
|
||||
opts *Options
|
||||
|
||||
Subscribers map[string][]*memorySubscriber
|
||||
|
||||
addr string
|
||||
sync.RWMutex
|
||||
connected bool
|
||||
}
|
||||
|
||||
type memoryEvent struct {
|
||||
err error
|
||||
message interface{}
|
||||
opts *Options
|
||||
topic string
|
||||
}
|
||||
|
||||
type memorySubscriber struct {
|
||||
opts SubscribeOptions
|
||||
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() 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 ...Option) error {
|
||||
for _, o := range opts {
|
||||
o(m.opts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryBroker) Publish(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,
|
||||
}
|
||||
|
||||
for _, sub := range subs {
|
||||
if err := sub.handler(p); err != nil {
|
||||
p.err = err
|
||||
if eh := m.opts.ErrorHandler; eh != nil {
|
||||
eh(p)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryBroker) Subscribe(topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
|
||||
m.RLock()
|
||||
if !m.connected {
|
||||
m.RUnlock()
|
||||
return nil, errors.New("not connected")
|
||||
}
|
||||
m.RUnlock()
|
||||
|
||||
var options 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 *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 {
|
||||
m.opts.Logger.Logf(log.ErrorLevel, "[memory]: failed to unmarshal: %v\n", 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() error {
|
||||
m.exit <- true
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMemoryBroker(opts ...Option) Broker {
|
||||
options := NewOptions(opts...)
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
return &memoryBroker{
|
||||
opts: options,
|
||||
Subscribers: make(map[string][]*memorySubscriber),
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"go-micro.dev/v4/codec"
|
||||
"go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/registry"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Codec codec.Marshaler
|
||||
|
||||
// Logger is the underlying logger
|
||||
Logger logger.Logger
|
||||
|
||||
// Registry used for clustering
|
||||
Registry registry.Registry
|
||||
// Other options for implementations of the interface
|
||||
// can be stored in a context
|
||||
Context context.Context
|
||||
|
||||
// Handler executed when error happens in broker mesage
|
||||
// processing
|
||||
ErrorHandler Handler
|
||||
|
||||
TLSConfig *tls.Config
|
||||
Addrs []string
|
||||
Secure bool
|
||||
}
|
||||
|
||||
type PublishOptions struct {
|
||||
// Other options for implementations of the interface
|
||||
// can be stored in a context
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
type SubscribeOptions struct {
|
||||
|
||||
// Other options for implementations of the interface
|
||||
// can be stored in a context
|
||||
Context context.Context
|
||||
// Subscribers with the same queue name
|
||||
// will create a shared subscription where each
|
||||
// receives a subset of messages.
|
||||
Queue string
|
||||
|
||||
// AutoAck defaults to true. When a handler returns
|
||||
// with a nil error the message is acked.
|
||||
AutoAck bool
|
||||
}
|
||||
|
||||
type Option func(*Options)
|
||||
|
||||
type PublishOption func(*PublishOptions)
|
||||
|
||||
// PublishContext set context.
|
||||
func PublishContext(ctx context.Context) PublishOption {
|
||||
return func(o *PublishOptions) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
type SubscribeOption func(*SubscribeOptions)
|
||||
|
||||
func NewOptions(opts ...Option) *Options {
|
||||
options := Options{
|
||||
Context: context.Background(),
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return &options
|
||||
}
|
||||
|
||||
func NewSubscribeOptions(opts ...SubscribeOption) SubscribeOptions {
|
||||
opt := SubscribeOptions{
|
||||
AutoAck: true,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Addrs sets the host addresses to be used by the broker.
|
||||
func Addrs(addrs ...string) Option {
|
||||
return func(o *Options) {
|
||||
o.Addrs = addrs
|
||||
}
|
||||
}
|
||||
|
||||
// Codec sets the codec used for encoding/decoding used where
|
||||
// a broker does not support headers.
|
||||
func Codec(c codec.Marshaler) Option {
|
||||
return func(o *Options) {
|
||||
o.Codec = c
|
||||
}
|
||||
}
|
||||
|
||||
// DisableAutoAck will disable auto acking of messages
|
||||
// after they have been handled.
|
||||
func DisableAutoAck() SubscribeOption {
|
||||
return func(o *SubscribeOptions) {
|
||||
o.AutoAck = false
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorHandler will catch all broker errors that cant be handled
|
||||
// in normal way, for example Codec errors.
|
||||
func ErrorHandler(h Handler) Option {
|
||||
return func(o *Options) {
|
||||
o.ErrorHandler = h
|
||||
}
|
||||
}
|
||||
|
||||
// Queue sets the name of the queue to share messages on.
|
||||
func Queue(name string) SubscribeOption {
|
||||
return func(o *SubscribeOptions) {
|
||||
o.Queue = name
|
||||
}
|
||||
}
|
||||
|
||||
func Registry(r registry.Registry) Option {
|
||||
return func(o *Options) {
|
||||
o.Registry = r
|
||||
}
|
||||
}
|
||||
|
||||
// Secure communication with the broker.
|
||||
func Secure(b bool) Option {
|
||||
return func(o *Options) {
|
||||
o.Secure = b
|
||||
}
|
||||
}
|
||||
|
||||
// Specify TLS Config.
|
||||
func TLSConfig(t *tls.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.TLSConfig = t
|
||||
}
|
||||
}
|
||||
|
||||
// Logger sets the underline logger.
|
||||
func Logger(l logger.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = l
|
||||
}
|
||||
}
|
||||
|
||||
// SubscribeContext set context.
|
||||
func SubscribeContext(ctx context.Context) SubscribeOption {
|
||||
return func(o *SubscribeOptions) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultCache is the default cache.
|
||||
DefaultCache Cache = NewCache()
|
||||
// DefaultExpiration is the default duration for items stored in
|
||||
// the cache to expire.
|
||||
DefaultExpiration time.Duration = 0
|
||||
|
||||
// ErrItemExpired is returned in Cache.Get when the item found in the cache
|
||||
// has expired.
|
||||
ErrItemExpired error = errors.New("item has expired")
|
||||
// ErrKeyNotFound is returned in Cache.Get and Cache.Delete when the
|
||||
// provided key could not be found in cache.
|
||||
ErrKeyNotFound error = errors.New("key not found in cache")
|
||||
)
|
||||
|
||||
// Cache is the interface that wraps the cache.
|
||||
type Cache interface {
|
||||
// Get gets a cached value by key.
|
||||
Get(ctx context.Context, key string) (interface{}, time.Time, error)
|
||||
// Put stores a key-value pair into cache.
|
||||
Put(ctx context.Context, key string, val interface{}, d time.Duration) error
|
||||
// Delete removes a key from cache.
|
||||
Delete(ctx context.Context, key string) error
|
||||
// String returns the name of the implementation.
|
||||
String() string
|
||||
}
|
||||
|
||||
// Item represents an item stored in the cache.
|
||||
type Item struct {
|
||||
Value interface{}
|
||||
Expiration int64
|
||||
}
|
||||
|
||||
// Expired returns true if the item has expired.
|
||||
func (i *Item) Expired() bool {
|
||||
if i.Expiration == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return time.Now().UnixNano() > i.Expiration
|
||||
}
|
||||
|
||||
// NewCache returns a new cache.
|
||||
func NewCache(opts ...Option) Cache {
|
||||
options := NewOptions(opts...)
|
||||
items := make(map[string]Item)
|
||||
|
||||
if len(options.Items) > 0 {
|
||||
items = options.Items
|
||||
}
|
||||
|
||||
return &memCache{
|
||||
opts: options,
|
||||
items: items,
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type memCache struct {
|
||||
opts Options
|
||||
|
||||
items map[string]Item
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (c *memCache) Get(ctx context.Context, key string) (interface{}, time.Time, error) {
|
||||
c.RWMutex.RLock()
|
||||
defer c.RWMutex.RUnlock()
|
||||
|
||||
item, found := c.items[key]
|
||||
if !found {
|
||||
return nil, time.Time{}, ErrKeyNotFound
|
||||
}
|
||||
if item.Expired() {
|
||||
return nil, time.Time{}, ErrItemExpired
|
||||
}
|
||||
|
||||
return item.Value, time.Unix(0, item.Expiration), nil
|
||||
}
|
||||
|
||||
func (c *memCache) Put(ctx context.Context, key string, val interface{}, d time.Duration) error {
|
||||
var e int64
|
||||
if d == DefaultExpiration {
|
||||
d = c.opts.Expiration
|
||||
}
|
||||
if d > 0 {
|
||||
e = time.Now().Add(d).UnixNano()
|
||||
}
|
||||
|
||||
c.RWMutex.Lock()
|
||||
defer c.RWMutex.Unlock()
|
||||
|
||||
c.items[key] = Item{
|
||||
Value: val,
|
||||
Expiration: e,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *memCache) Delete(ctx context.Context, key string) error {
|
||||
c.RWMutex.Lock()
|
||||
defer c.RWMutex.Unlock()
|
||||
|
||||
_, found := c.items[key]
|
||||
if !found {
|
||||
return ErrKeyNotFound
|
||||
}
|
||||
|
||||
delete(c.items, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memCache) String() string {
|
||||
return "memory"
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
// Options represents the options for the cache.
|
||||
type Options struct {
|
||||
// Context should contain all implementation specific options, using context.WithValue.
|
||||
Context context.Context
|
||||
// Logger is the be used logger
|
||||
Logger logger.Logger
|
||||
Items map[string]Item
|
||||
// Address represents the address or other connection information of the cache service.
|
||||
Address string
|
||||
Expiration time.Duration
|
||||
}
|
||||
|
||||
// Option manipulates the Options passed.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Expiration sets the duration for items stored in the cache to expire.
|
||||
func Expiration(d time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.Expiration = d
|
||||
}
|
||||
}
|
||||
|
||||
// Items initializes the cache with preconfigured items.
|
||||
func Items(i map[string]Item) Option {
|
||||
return func(o *Options) {
|
||||
o.Items = i
|
||||
}
|
||||
}
|
||||
|
||||
// WithAddress sets the cache service address or connection information.
|
||||
func WithAddress(addr string) Option {
|
||||
return func(o *Options) {
|
||||
o.Address = addr
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext sets the cache context, for any extra configuration.
|
||||
func WithContext(c context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = c
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger sets underline logger.
|
||||
func WithLogger(l logger.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = l
|
||||
}
|
||||
}
|
||||
|
||||
// NewOptions returns a new options struct.
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{
|
||||
Expiration: DefaultExpiration,
|
||||
Items: make(map[string]Item),
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v4/util/backoff"
|
||||
)
|
||||
|
||||
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) {
|
||||
return backoff.Do(attempts), nil
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"time"
|
||||
|
||||
cache "github.com/patrickmn/go-cache"
|
||||
|
||||
"go-micro.dev/v4/metadata"
|
||||
"go-micro.dev/v4/transport/headers"
|
||||
)
|
||||
|
||||
// NewCache returns an initialized cache.
|
||||
func NewCache() *Cache {
|
||||
return &Cache{
|
||||
cache: cache.New(cache.NoExpiration, 30*time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
// Cache for responses.
|
||||
type Cache struct {
|
||||
cache *cache.Cache
|
||||
}
|
||||
|
||||
// Get a response from the cache.
|
||||
func (c *Cache) Get(ctx context.Context, req *Request) (interface{}, bool) {
|
||||
return c.cache.Get(key(ctx, req))
|
||||
}
|
||||
|
||||
// Set a response in the cache.
|
||||
func (c *Cache) Set(ctx context.Context, req *Request, rsp interface{}, expiry time.Duration) {
|
||||
c.cache.Set(key(ctx, req), rsp, expiry)
|
||||
}
|
||||
|
||||
// List the key value pairs in the cache.
|
||||
func (c *Cache) List() map[string]string {
|
||||
items := c.cache.Items()
|
||||
|
||||
rsp := make(map[string]string, len(items))
|
||||
|
||||
for k, v := range items {
|
||||
bytes, _ := json.Marshal(v.Object)
|
||||
rsp[k] = string(bytes)
|
||||
}
|
||||
|
||||
return rsp
|
||||
}
|
||||
|
||||
// key returns a hash for the context and request.
|
||||
func key(ctx context.Context, req *Request) string {
|
||||
ns, _ := metadata.Get(ctx, headers.Namespace)
|
||||
|
||||
bytes, _ := json.Marshal(map[string]interface{}{
|
||||
"namespace": ns,
|
||||
"request": map[string]interface{}{
|
||||
"service": (*req).Service(),
|
||||
"endpoint": (*req).Endpoint(),
|
||||
"method": (*req).Method(),
|
||||
"body": (*req).Body(),
|
||||
},
|
||||
})
|
||||
|
||||
h := fnv.New64()
|
||||
h.Write(bytes)
|
||||
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
// Package client is an interface for an RPC client
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
var (
|
||||
// NewClient returns a new client.
|
||||
NewClient func(...Option) Client = newRPCClient
|
||||
// DefaultClient is a default client to use out of the box.
|
||||
DefaultClient Client = newRPCClient()
|
||||
)
|
||||
|
||||
// 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
|
||||
Options() Options
|
||||
NewMessage(topic string, msg interface{}, opts ...MessageOption) Message
|
||||
NewRequest(service, endpoint string, req interface{}, reqOpts ...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
|
||||
String() string
|
||||
}
|
||||
|
||||
// Router manages request routing.
|
||||
type Router interface {
|
||||
SendRequest(context.Context, Request) (Response, error)
|
||||
}
|
||||
|
||||
// Message is the interface for publishing asynchronously.
|
||||
type Message interface {
|
||||
Topic() string
|
||||
Payload() interface{}
|
||||
ContentType() string
|
||||
}
|
||||
|
||||
// Request is the interface for a synchronous request used by Call or Stream.
|
||||
type Request interface {
|
||||
// The service to call
|
||||
Service() string
|
||||
// The action to take
|
||||
Method() string
|
||||
// The endpoint to invoke
|
||||
Endpoint() string
|
||||
// The content type
|
||||
ContentType() string
|
||||
// The unencoded request body
|
||||
Body() interface{}
|
||||
// Write to the encoded request writer. This is nil before a call is made
|
||||
Codec() codec.Writer
|
||||
// indicates whether the request will be a streaming one rather than unary
|
||||
Stream() bool
|
||||
}
|
||||
|
||||
// Response is the response received from a service.
|
||||
type Response interface {
|
||||
// Read the response
|
||||
Codec() codec.Reader
|
||||
// read the header
|
||||
Header() map[string]string
|
||||
// Read the undecoded response
|
||||
Read() ([]byte, error)
|
||||
}
|
||||
|
||||
// Stream is the inteface for a bidirectional synchronous stream.
|
||||
type Stream interface {
|
||||
Closer
|
||||
// Context for the stream
|
||||
Context() context.Context
|
||||
// The request made
|
||||
Request() Request
|
||||
// The response read
|
||||
Response() Response
|
||||
// Send will encode and send a request
|
||||
Send(interface{}) error
|
||||
// Recv will decode and read a response
|
||||
Recv(interface{}) error
|
||||
// Error returns the stream error
|
||||
Error() error
|
||||
// Close closes the stream
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Closer handle client close.
|
||||
type Closer interface {
|
||||
// CloseSend closes the send direction of the stream.
|
||||
CloseSend() error
|
||||
}
|
||||
|
||||
// Option used by the Client.
|
||||
type Option func(*Options)
|
||||
|
||||
// CallOption used by Call or Stream.
|
||||
type CallOption func(*CallOptions)
|
||||
|
||||
// PublishOption used by Publish.
|
||||
type PublishOption func(*PublishOptions)
|
||||
|
||||
// MessageOption used by NewMessage.
|
||||
type MessageOption func(*MessageOptions)
|
||||
|
||||
// RequestOption used by NewRequest.
|
||||
type RequestOption func(*RequestOptions)
|
||||
|
||||
// Makes a synchronous call to a service using the default client.
|
||||
func Call(ctx context.Context, request Request, response interface{}, opts ...CallOption) error {
|
||||
return DefaultClient.Call(ctx, request, response, opts...)
|
||||
}
|
||||
|
||||
// Publishes a publication using the default client. Using the underlying broker
|
||||
// set within the options.
|
||||
func Publish(ctx context.Context, msg Message, opts ...PublishOption) error {
|
||||
return DefaultClient.Publish(ctx, msg, opts...)
|
||||
}
|
||||
|
||||
// Creates a new message using the default client.
|
||||
func NewMessage(topic string, payload interface{}, opts ...MessageOption) Message {
|
||||
return DefaultClient.NewMessage(topic, payload, opts...)
|
||||
}
|
||||
|
||||
// Creates a new request using the default client. Content Type will
|
||||
// be set to the default within options and use the appropriate codec.
|
||||
func NewRequest(service, endpoint string, request interface{}, reqOpts ...RequestOption) Request {
|
||||
return DefaultClient.NewRequest(service, endpoint, request, reqOpts...)
|
||||
}
|
||||
|
||||
// Creates a streaming connection with a service and returns responses on the
|
||||
// channel passed in. It's up to the user to close the streamer.
|
||||
func NewStream(ctx context.Context, request Request, opts ...CallOption) (Stream, error) {
|
||||
return DefaultClient.Stream(ctx, request, opts...)
|
||||
}
|
||||
|
||||
func String() string {
|
||||
return DefaultClient.String()
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type clientKey struct{}
|
||||
|
||||
func FromContext(ctx context.Context) (Client, bool) {
|
||||
c, ok := ctx.Value(clientKey{}).(Client)
|
||||
return c, ok
|
||||
}
|
||||
|
||||
func NewContext(ctx context.Context, c Client) context.Context {
|
||||
return context.WithValue(ctx, clientKey{}, c)
|
||||
}
|
||||
+412
@@ -0,0 +1,412 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v4/broker"
|
||||
"go-micro.dev/v4/codec"
|
||||
"go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/selector"
|
||||
"go-micro.dev/v4/transport"
|
||||
)
|
||||
|
||||
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 = 5
|
||||
// DefaultRequestTimeout is the default request timeout.
|
||||
DefaultRequestTimeout = time.Second * 30
|
||||
// DefaultConnectionTimeout is the default connection timeout.
|
||||
DefaultConnectionTimeout = time.Second * 5
|
||||
// DefaultPoolSize sets the connection pool size.
|
||||
DefaultPoolSize = 100
|
||||
// DefaultPoolTTL sets the connection pool ttl.
|
||||
DefaultPoolTTL = time.Minute
|
||||
)
|
||||
|
||||
// Options are the Client options.
|
||||
type Options struct {
|
||||
|
||||
// Default Call Options
|
||||
CallOptions CallOptions
|
||||
|
||||
// Router sets the router
|
||||
Router Router
|
||||
|
||||
Registry registry.Registry
|
||||
Selector selector.Selector
|
||||
Transport transport.Transport
|
||||
|
||||
// Plugged interfaces
|
||||
Broker broker.Broker
|
||||
|
||||
// Logger is the underline logger
|
||||
Logger logger.Logger
|
||||
|
||||
// Other options for implementations of the interface
|
||||
// can be stored in a context
|
||||
Context context.Context
|
||||
Codecs map[string]codec.NewCodec
|
||||
|
||||
// Response cache
|
||||
Cache *Cache
|
||||
|
||||
// Used to select codec
|
||||
ContentType string
|
||||
|
||||
// Middleware for client
|
||||
Wrappers []Wrapper
|
||||
|
||||
// Connection Pool
|
||||
PoolSize int
|
||||
PoolTTL time.Duration
|
||||
}
|
||||
|
||||
// CallOptions are options used to make calls to a server.
|
||||
type CallOptions struct {
|
||||
|
||||
// Other options for implementations of the interface
|
||||
// can be stored in a context
|
||||
Context context.Context
|
||||
// Backoff func
|
||||
Backoff BackoffFunc
|
||||
// Check if retriable func
|
||||
Retry RetryFunc
|
||||
SelectOptions []selector.SelectOption
|
||||
|
||||
// Address of remote hosts
|
||||
Address []string
|
||||
|
||||
// Middleware for low level call func
|
||||
CallWrappers []CallWrapper
|
||||
|
||||
// ConnectionTimeout of one request to the server.
|
||||
// Set this lower than the RequestTimeout to enbale retries on connection timeout.
|
||||
ConnectionTimeout time.Duration
|
||||
// Request/Response timeout of entire srv.Call, for single request timeout set ConnectionTimeout.
|
||||
RequestTimeout time.Duration
|
||||
// Stream timeout for the stream
|
||||
StreamTimeout time.Duration
|
||||
// Duration to cache the response for
|
||||
CacheExpiry time.Duration
|
||||
// Transport Dial Timeout. Used for initial dial to establish a connection.
|
||||
DialTimeout time.Duration
|
||||
// Number of Call attempts
|
||||
Retries int
|
||||
// Use the services own auth token
|
||||
ServiceToken bool
|
||||
// ConnClose sets the Connection: close header.
|
||||
ConnClose bool
|
||||
}
|
||||
|
||||
type PublishOptions struct {
|
||||
// Other options for implementations of the interface
|
||||
// can be stored in a context
|
||||
Context context.Context
|
||||
// Exchange is the routing exchange for the message
|
||||
Exchange string
|
||||
}
|
||||
|
||||
type MessageOptions struct {
|
||||
ContentType string
|
||||
}
|
||||
|
||||
type RequestOptions struct {
|
||||
|
||||
// Other options for implementations of the interface
|
||||
// can be stored in a context
|
||||
Context context.Context
|
||||
ContentType string
|
||||
Stream bool
|
||||
}
|
||||
|
||||
// NewOptions creates new Client options.
|
||||
func NewOptions(options ...Option) Options {
|
||||
opts := Options{
|
||||
Cache: NewCache(),
|
||||
Context: context.Background(),
|
||||
ContentType: DefaultContentType,
|
||||
Codecs: make(map[string]codec.NewCodec),
|
||||
CallOptions: CallOptions{
|
||||
Backoff: DefaultBackoff,
|
||||
Retry: DefaultRetry,
|
||||
Retries: DefaultRetries,
|
||||
RequestTimeout: DefaultRequestTimeout,
|
||||
ConnectionTimeout: DefaultConnectionTimeout,
|
||||
DialTimeout: transport.DefaultDialTimeout,
|
||||
},
|
||||
PoolSize: DefaultPoolSize,
|
||||
PoolTTL: DefaultPoolTTL,
|
||||
Broker: broker.DefaultBroker,
|
||||
Selector: selector.DefaultSelector,
|
||||
Registry: registry.DefaultRegistry,
|
||||
Transport: transport.DefaultTransport,
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range options {
|
||||
o(&opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// Broker to be used for pub/sub.
|
||||
func Broker(b broker.Broker) Option {
|
||||
return func(o *Options) {
|
||||
o.Broker = b
|
||||
}
|
||||
}
|
||||
|
||||
// Codec to be used to encode/decode requests for a given content type.
|
||||
func Codec(contentType string, c codec.NewCodec) Option {
|
||||
return func(o *Options) {
|
||||
o.Codecs[contentType] = c
|
||||
}
|
||||
}
|
||||
|
||||
// ContentType sets the default content type of the client.
|
||||
func ContentType(ct string) Option {
|
||||
return func(o *Options) {
|
||||
o.ContentType = ct
|
||||
}
|
||||
}
|
||||
|
||||
// PoolSize sets the connection pool size.
|
||||
func PoolSize(d int) Option {
|
||||
return func(o *Options) {
|
||||
o.PoolSize = d
|
||||
}
|
||||
}
|
||||
|
||||
// PoolTTL sets the connection pool ttl.
|
||||
func PoolTTL(d time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.PoolTTL = d
|
||||
}
|
||||
}
|
||||
|
||||
// Registry to find nodes for a given service.
|
||||
func Registry(r registry.Registry) Option {
|
||||
return func(o *Options) {
|
||||
o.Registry = r
|
||||
// set in the selector
|
||||
o.Selector.Init(selector.Registry(r))
|
||||
}
|
||||
}
|
||||
|
||||
// Transport to use for communication e.g http, rabbitmq, etc.
|
||||
func Transport(t transport.Transport) Option {
|
||||
return func(o *Options) {
|
||||
o.Transport = t
|
||||
}
|
||||
}
|
||||
|
||||
// Select is used to select a node to route a request to.
|
||||
func Selector(s selector.Selector) Option {
|
||||
return func(o *Options) {
|
||||
o.Selector = s
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a Wrapper to a list of options passed into the client.
|
||||
func Wrap(w Wrapper) Option {
|
||||
return func(o *Options) {
|
||||
o.Wrappers = append(o.Wrappers, w)
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a Wrapper to the list of CallFunc wrappers.
|
||||
func WrapCall(cw ...CallWrapper) Option {
|
||||
return func(o *Options) {
|
||||
o.CallOptions.CallWrappers = append(o.CallOptions.CallWrappers, cw...)
|
||||
}
|
||||
}
|
||||
|
||||
// Backoff is used to set the backoff function used
|
||||
// when retrying Calls.
|
||||
func Backoff(fn BackoffFunc) Option {
|
||||
return func(o *Options) {
|
||||
o.CallOptions.Backoff = fn
|
||||
}
|
||||
}
|
||||
|
||||
// Retries set the number of retries when making the request.
|
||||
func Retries(i int) Option {
|
||||
return func(o *Options) {
|
||||
o.CallOptions.Retries = i
|
||||
}
|
||||
}
|
||||
|
||||
// Retry sets the retry function to be used when re-trying.
|
||||
func Retry(fn RetryFunc) Option {
|
||||
return func(o *Options) {
|
||||
o.CallOptions.Retry = fn
|
||||
}
|
||||
}
|
||||
|
||||
// RequestTimeout set the request timeout.
|
||||
func RequestTimeout(d time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.CallOptions.RequestTimeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// StreamTimeout sets the stream timeout.
|
||||
func StreamTimeout(d time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.CallOptions.StreamTimeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// DialTimeout sets the transport dial timeout.
|
||||
func DialTimeout(d time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.CallOptions.DialTimeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// Call Options
|
||||
|
||||
// WithExchange sets the exchange to route a message through.
|
||||
func WithExchange(e string) PublishOption {
|
||||
return func(o *PublishOptions) {
|
||||
o.Exchange = e
|
||||
}
|
||||
}
|
||||
|
||||
// PublishContext sets the context in publish options.
|
||||
func PublishContext(ctx context.Context) PublishOption {
|
||||
return func(o *PublishOptions) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// WithAddress sets the remote addresses to use rather than using service discovery.
|
||||
func WithAddress(a ...string) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.Address = a
|
||||
}
|
||||
}
|
||||
|
||||
func WithSelectOption(so ...selector.SelectOption) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.SelectOptions = append(o.SelectOptions, so...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithCallWrapper is a CallOption which adds to the existing CallFunc wrappers.
|
||||
func WithCallWrapper(cw ...CallWrapper) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.CallWrappers = append(o.CallWrappers, cw...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithBackoff is a CallOption which overrides that which
|
||||
// set in Options.CallOptions.
|
||||
func WithBackoff(fn BackoffFunc) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.Backoff = fn
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetry is a CallOption which overrides that which
|
||||
// set in Options.CallOptions.
|
||||
func WithRetry(fn RetryFunc) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.Retry = fn
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetries sets the number of tries for a call.
|
||||
// This CallOption overrides Options.CallOptions.
|
||||
func WithRetries(i int) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.Retries = i
|
||||
}
|
||||
}
|
||||
|
||||
// WithRequestTimeout is a CallOption which overrides that which
|
||||
// set in Options.CallOptions.
|
||||
func WithRequestTimeout(d time.Duration) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.RequestTimeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithConnClose sets the Connection header to close.
|
||||
func WithConnClose() CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.ConnClose = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithStreamTimeout sets the stream timeout.
|
||||
func WithStreamTimeout(d time.Duration) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.StreamTimeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithDialTimeout is a CallOption which overrides that which
|
||||
// set in Options.CallOptions.
|
||||
func WithDialTimeout(d time.Duration) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.DialTimeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithServiceToken is a CallOption which overrides the
|
||||
// authorization header with the services own auth token.
|
||||
func WithServiceToken() CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.ServiceToken = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithCache is a CallOption which sets the duration the response
|
||||
// shoull be cached for.
|
||||
func WithCache(c time.Duration) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.CacheExpiry = c
|
||||
}
|
||||
}
|
||||
|
||||
func WithMessageContentType(ct string) MessageOption {
|
||||
return func(o *MessageOptions) {
|
||||
o.ContentType = ct
|
||||
}
|
||||
}
|
||||
|
||||
// Request Options
|
||||
|
||||
func WithContentType(ct string) RequestOption {
|
||||
return func(o *RequestOptions) {
|
||||
o.ContentType = ct
|
||||
}
|
||||
}
|
||||
|
||||
func StreamingRequest() RequestOption {
|
||||
return func(o *RequestOptions) {
|
||||
o.Stream = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithRouter sets the client router.
|
||||
func WithRouter(r Router) Option {
|
||||
return func(o *Options) {
|
||||
o.Router = r
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger sets the underline logger.
|
||||
func WithLogger(l logger.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = l
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/errors"
|
||||
)
|
||||
|
||||
// note that returning either false or a non-nil error will result in the call not being retried.
|
||||
type RetryFunc func(ctx context.Context, req Request, retryCount int, err error) (bool, error)
|
||||
|
||||
// RetryAlways always retry on error.
|
||||
func RetryAlways(ctx context.Context, req Request, retryCount int, err error) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RetryOnError retries a request on a 500 or timeout error.
|
||||
func RetryOnError(ctx context.Context, req Request, retryCount int, err error) (bool, error) {
|
||||
if err == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
e := errors.Parse(err.Error())
|
||||
if e == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
switch e.Code {
|
||||
// Retry on timeout, not on 500 internal server error, as that is a business
|
||||
// logic error that should be handled by the user.
|
||||
case 408:
|
||||
return true, nil
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
+736
@@ -0,0 +1,736 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"go-micro.dev/v4/broker"
|
||||
"go-micro.dev/v4/codec"
|
||||
raw "go-micro.dev/v4/codec/bytes"
|
||||
merrors "go-micro.dev/v4/errors"
|
||||
log "go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/metadata"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/selector"
|
||||
"go-micro.dev/v4/transport"
|
||||
"go-micro.dev/v4/transport/headers"
|
||||
"go-micro.dev/v4/util/buf"
|
||||
"go-micro.dev/v4/util/net"
|
||||
"go-micro.dev/v4/util/pool"
|
||||
)
|
||||
|
||||
const (
|
||||
packageID = "go.micro.client"
|
||||
)
|
||||
|
||||
type rpcClient struct {
|
||||
opts Options
|
||||
once atomic.Value
|
||||
pool pool.Pool
|
||||
|
||||
seq uint64
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func newRPCClient(opt ...Option) Client {
|
||||
opts := NewOptions(opt...)
|
||||
|
||||
p := pool.NewPool(
|
||||
pool.Size(opts.PoolSize),
|
||||
pool.TTL(opts.PoolTTL),
|
||||
pool.Transport(opts.Transport),
|
||||
)
|
||||
|
||||
rc := &rpcClient{
|
||||
opts: opts,
|
||||
pool: p,
|
||||
seq: 0,
|
||||
}
|
||||
rc.once.Store(false)
|
||||
|
||||
c := Client(rc)
|
||||
|
||||
// wrap in reverse
|
||||
for i := len(opts.Wrappers); i > 0; i-- {
|
||||
c = opts.Wrappers[i-1](c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (r *rpcClient) newCodec(contentType string) (codec.NewCodec, error) {
|
||||
if c, ok := r.opts.Codecs[contentType]; ok {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
if cf, ok := DefaultCodecs[contentType]; ok {
|
||||
return cf, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported Content-Type: %s", contentType)
|
||||
}
|
||||
|
||||
func (r *rpcClient) call(
|
||||
ctx context.Context,
|
||||
node *registry.Node,
|
||||
req Request,
|
||||
resp interface{},
|
||||
opts CallOptions,
|
||||
) error {
|
||||
address := node.Address
|
||||
logger := r.Options().Logger
|
||||
|
||||
msg := &transport.Message{
|
||||
Header: make(map[string]string),
|
||||
}
|
||||
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if ok {
|
||||
for k, v := range md {
|
||||
// Don't copy Micro-Topic header, that is used for pub/sub
|
||||
// this is fixes the case when the client uses the same context that
|
||||
// is received in the subscriber.
|
||||
if k == headers.Message {
|
||||
continue
|
||||
}
|
||||
|
||||
msg.Header[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Set connection timeout for single requests to the server. Should be > 0
|
||||
// as otherwise requests can't be made.
|
||||
cTimeout := opts.ConnectionTimeout
|
||||
if cTimeout == 0 {
|
||||
logger.Log(log.DebugLevel, "connection timeout was set to 0, overridng to default connection timeout")
|
||||
|
||||
cTimeout = DefaultConnectionTimeout
|
||||
}
|
||||
|
||||
// set timeout in nanoseconds
|
||||
msg.Header["Timeout"] = fmt.Sprintf("%d", cTimeout)
|
||||
// set the content type for the request
|
||||
msg.Header["Content-Type"] = req.ContentType()
|
||||
// set the accept header
|
||||
msg.Header["Accept"] = req.ContentType()
|
||||
|
||||
// setup old protocol
|
||||
reqCodec := setupProtocol(msg, node)
|
||||
|
||||
// no codec specified
|
||||
if reqCodec == nil {
|
||||
var err error
|
||||
reqCodec, err = r.newCodec(req.ContentType())
|
||||
|
||||
if err != nil {
|
||||
return merrors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
dOpts := []transport.DialOption{
|
||||
transport.WithStream(),
|
||||
}
|
||||
|
||||
if opts.DialTimeout >= 0 {
|
||||
dOpts = append(dOpts, transport.WithTimeout(opts.DialTimeout))
|
||||
}
|
||||
|
||||
if opts.ConnClose {
|
||||
dOpts = append(dOpts, transport.WithConnClose())
|
||||
}
|
||||
|
||||
c, err := r.pool.Get(address, dOpts...)
|
||||
if err != nil {
|
||||
return merrors.InternalServerError("go.micro.client", "connection error: %v", err)
|
||||
}
|
||||
|
||||
seq := atomic.AddUint64(&r.seq, 1) - 1
|
||||
codec := newRPCCodec(msg, c, reqCodec, "")
|
||||
|
||||
rsp := &rpcResponse{
|
||||
socket: c,
|
||||
codec: codec,
|
||||
}
|
||||
|
||||
releaseFunc := func(err error) {
|
||||
if err = r.pool.Release(c, err); err != nil {
|
||||
logger.Log(log.ErrorLevel, "failed to release pool", err)
|
||||
}
|
||||
}
|
||||
|
||||
stream := &rpcStream{
|
||||
id: fmt.Sprintf("%v", seq),
|
||||
context: ctx,
|
||||
request: req,
|
||||
response: rsp,
|
||||
codec: codec,
|
||||
closed: make(chan bool),
|
||||
close: opts.ConnClose,
|
||||
release: releaseFunc,
|
||||
sendEOS: false,
|
||||
}
|
||||
|
||||
// close the stream on exiting this function
|
||||
defer func() {
|
||||
if err := stream.Close(); err != nil {
|
||||
logger.Log(log.ErrorLevel, "failed to close stream", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// wait for error response
|
||||
ch := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ch <- merrors.InternalServerError("go.micro.client", "panic recovered: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// send request
|
||||
if err := stream.Send(req.Body()); err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
// recv response
|
||||
if err := stream.Recv(resp); err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
// success
|
||||
ch <- nil
|
||||
}()
|
||||
|
||||
var grr error
|
||||
|
||||
select {
|
||||
case err := <-ch:
|
||||
return err
|
||||
case <-time.After(cTimeout):
|
||||
grr = merrors.Timeout("go.micro.client", fmt.Sprintf("%v", ctx.Err()))
|
||||
}
|
||||
|
||||
// set the stream error
|
||||
if grr != nil {
|
||||
stream.Lock()
|
||||
stream.err = grr
|
||||
stream.Unlock()
|
||||
|
||||
return grr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rpcClient) stream(ctx context.Context, node *registry.Node, req Request, opts CallOptions) (Stream, error) {
|
||||
address := node.Address
|
||||
logger := r.Options().Logger
|
||||
|
||||
msg := &transport.Message{
|
||||
Header: make(map[string]string),
|
||||
}
|
||||
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if ok {
|
||||
for k, v := range md {
|
||||
msg.Header[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// set timeout in nanoseconds
|
||||
if opts.StreamTimeout > time.Duration(0) {
|
||||
msg.Header["Timeout"] = fmt.Sprintf("%d", opts.StreamTimeout)
|
||||
}
|
||||
// set the content type for the request
|
||||
msg.Header["Content-Type"] = req.ContentType()
|
||||
// set the accept header
|
||||
msg.Header["Accept"] = req.ContentType()
|
||||
|
||||
// set old codecs
|
||||
nCodec := setupProtocol(msg, node)
|
||||
|
||||
// no codec specified
|
||||
if nCodec == nil {
|
||||
var err error
|
||||
|
||||
nCodec, err = r.newCodec(req.ContentType())
|
||||
if err != nil {
|
||||
return nil, merrors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
dOpts := []transport.DialOption{
|
||||
transport.WithStream(),
|
||||
}
|
||||
|
||||
if opts.DialTimeout >= 0 {
|
||||
dOpts = append(dOpts, transport.WithTimeout(opts.DialTimeout))
|
||||
}
|
||||
|
||||
c, err := r.opts.Transport.Dial(address, dOpts...)
|
||||
if err != nil {
|
||||
return nil, merrors.InternalServerError("go.micro.client", "connection error: %v", err)
|
||||
}
|
||||
|
||||
// increment the sequence number
|
||||
seq := atomic.AddUint64(&r.seq, 1) - 1
|
||||
id := fmt.Sprintf("%v", seq)
|
||||
|
||||
// create codec with stream id
|
||||
codec := newRPCCodec(msg, c, nCodec, id)
|
||||
|
||||
rsp := &rpcResponse{
|
||||
socket: c,
|
||||
codec: codec,
|
||||
}
|
||||
|
||||
// set request codec
|
||||
if r, ok := req.(*rpcRequest); ok {
|
||||
r.codec = codec
|
||||
}
|
||||
|
||||
stream := &rpcStream{
|
||||
id: id,
|
||||
context: ctx,
|
||||
request: req,
|
||||
response: rsp,
|
||||
codec: codec,
|
||||
// used to close the stream
|
||||
closed: make(chan bool),
|
||||
// signal the end of stream,
|
||||
sendEOS: true,
|
||||
release: func(_ error) {},
|
||||
}
|
||||
|
||||
// wait for error response
|
||||
ch := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
// send the first message
|
||||
ch <- stream.Send(req.Body())
|
||||
}()
|
||||
|
||||
var grr error
|
||||
|
||||
select {
|
||||
case err := <-ch:
|
||||
grr = err
|
||||
case <-ctx.Done():
|
||||
grr = merrors.Timeout("go.micro.client", fmt.Sprintf("%v", ctx.Err()))
|
||||
}
|
||||
|
||||
if grr != nil {
|
||||
// set the error
|
||||
stream.Lock()
|
||||
stream.err = grr
|
||||
stream.Unlock()
|
||||
|
||||
// close the stream
|
||||
if err := stream.Close(); err != nil {
|
||||
logger.Logf(log.ErrorLevel, "failed to close stream: %v", err)
|
||||
}
|
||||
|
||||
return nil, grr
|
||||
}
|
||||
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
func (r *rpcClient) Init(opts ...Option) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
size := r.opts.PoolSize
|
||||
ttl := r.opts.PoolTTL
|
||||
tr := r.opts.Transport
|
||||
|
||||
for _, o := range opts {
|
||||
o(&r.opts)
|
||||
}
|
||||
|
||||
// update pool configuration if the options changed
|
||||
if size != r.opts.PoolSize || ttl != r.opts.PoolTTL || tr != r.opts.Transport {
|
||||
// close existing pool
|
||||
if err := r.pool.Close(); err != nil {
|
||||
return errors.Wrap(err, "failed to close pool")
|
||||
}
|
||||
|
||||
// create new pool
|
||||
r.pool = pool.NewPool(
|
||||
pool.Size(r.opts.PoolSize),
|
||||
pool.TTL(r.opts.PoolTTL),
|
||||
pool.Transport(r.opts.Transport),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Options retrives the options.
|
||||
func (r *rpcClient) Options() Options {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
return r.opts
|
||||
}
|
||||
|
||||
// next returns an iterator for the next nodes to call.
|
||||
func (r *rpcClient) next(request Request, opts CallOptions) (selector.Next, error) {
|
||||
// try get the proxy
|
||||
service, address, _ := net.Proxy(request.Service(), opts.Address)
|
||||
|
||||
// return remote address
|
||||
if len(address) > 0 {
|
||||
nodes := make([]*registry.Node, len(address))
|
||||
|
||||
for i, addr := range address {
|
||||
nodes[i] = ®istry.Node{
|
||||
Address: addr,
|
||||
// Set the protocol
|
||||
Metadata: map[string]string{
|
||||
"protocol": "mucp",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// crude return method
|
||||
return func() (*registry.Node, error) {
|
||||
return nodes[time.Now().Unix()%int64(len(nodes))], nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// get next nodes from the selector
|
||||
next, err := r.opts.Selector.Select(service, opts.SelectOptions...)
|
||||
if err != nil {
|
||||
if errors.Is(err, selector.ErrNotFound) {
|
||||
return nil, merrors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
|
||||
}
|
||||
|
||||
return nil, merrors.InternalServerError("go.micro.client", "error selecting %s node: %s", service, err.Error())
|
||||
}
|
||||
|
||||
return next, nil
|
||||
}
|
||||
|
||||
func (r *rpcClient) Call(ctx context.Context, request Request, response interface{}, opts ...CallOption) error {
|
||||
// TODO: further validate these mutex locks. full lock would prevent
|
||||
// parallel calls. Maybe we can set individual locks for secctions.
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
// make a copy of call opts
|
||||
callOpts := r.opts.CallOptions
|
||||
for _, opt := range opts {
|
||||
opt(&callOpts)
|
||||
}
|
||||
|
||||
next, err := r.next(request, callOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 := WithRequestTimeout(time.Until(d))
|
||||
opt(&callOpts)
|
||||
}
|
||||
|
||||
// should we noop right here?
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return merrors.Timeout("go.micro.client", fmt.Sprintf("%v", ctx.Err()))
|
||||
default:
|
||||
}
|
||||
|
||||
// make copy of call method
|
||||
rcall := r.call
|
||||
|
||||
// wrap the call in reverse
|
||||
for i := len(callOpts.CallWrappers); i > 0; i-- {
|
||||
rcall = callOpts.CallWrappers[i-1](rcall)
|
||||
}
|
||||
|
||||
// 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, request, i)
|
||||
if err != nil {
|
||||
return merrors.InternalServerError("go.micro.client", "backoff error: %v", err.Error())
|
||||
}
|
||||
|
||||
// only sleep if greater than 0
|
||||
if t.Seconds() > 0 {
|
||||
time.Sleep(t)
|
||||
}
|
||||
|
||||
// select next node
|
||||
node, err := next()
|
||||
service := request.Service()
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, selector.ErrNotFound) {
|
||||
return merrors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
|
||||
}
|
||||
|
||||
return merrors.InternalServerError("go.micro.client",
|
||||
"error getting next %s node: %s",
|
||||
service,
|
||||
err.Error())
|
||||
}
|
||||
|
||||
// make the call
|
||||
err = rcall(ctx, node, request, response, callOpts)
|
||||
r.opts.Selector.Mark(service, node, err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// get the retries
|
||||
retries := callOpts.Retries
|
||||
|
||||
// disable retries when using a proxy
|
||||
// Note: I don't see why we should disable retries for proxies, so commenting out.
|
||||
// if _, _, ok := net.Proxy(request.Service(), callOpts.Address); ok {
|
||||
// retries = 0
|
||||
// }
|
||||
|
||||
ch := make(chan error, retries+1)
|
||||
|
||||
var gerr error
|
||||
|
||||
for i := 0; i <= retries; i++ {
|
||||
go func(i int) {
|
||||
ch <- call(i)
|
||||
}(i)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return merrors.Timeout("go.micro.client", fmt.Sprintf("call timeout: %v", ctx.Err()))
|
||||
case err := <-ch:
|
||||
// if the call succeeded lets bail early
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
retry, rerr := callOpts.Retry(ctx, request, i, err)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
|
||||
if !retry {
|
||||
return err
|
||||
}
|
||||
|
||||
r.opts.Logger.Logf(log.DebugLevel, "Retrying request. Previous attempt failed with: %v", err)
|
||||
|
||||
gerr = err
|
||||
}
|
||||
}
|
||||
|
||||
return gerr
|
||||
}
|
||||
|
||||
func (r *rpcClient) Stream(ctx context.Context, request Request, opts ...CallOption) (Stream, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
// make a copy of call opts
|
||||
callOpts := r.opts.CallOptions
|
||||
for _, opt := range opts {
|
||||
opt(&callOpts)
|
||||
}
|
||||
|
||||
next, err := r.next(request, callOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, merrors.Timeout("go.micro.client", fmt.Sprintf("%v", ctx.Err()))
|
||||
default:
|
||||
}
|
||||
|
||||
call := func(i int) (Stream, error) {
|
||||
// call backoff first. Someone may want an initial start delay
|
||||
t, err := callOpts.Backoff(ctx, request, i)
|
||||
if err != nil {
|
||||
return nil, merrors.InternalServerError("go.micro.client", "backoff error: %v", err.Error())
|
||||
}
|
||||
|
||||
// only sleep if greater than 0
|
||||
if t.Seconds() > 0 {
|
||||
time.Sleep(t)
|
||||
}
|
||||
|
||||
node, err := next()
|
||||
service := request.Service()
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, selector.ErrNotFound) {
|
||||
return nil, merrors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
|
||||
}
|
||||
|
||||
return nil, merrors.InternalServerError("go.micro.client",
|
||||
"error getting next %s node: %s",
|
||||
service,
|
||||
err.Error())
|
||||
}
|
||||
|
||||
stream, err := r.stream(ctx, node, request, callOpts)
|
||||
r.opts.Selector.Mark(service, node, err)
|
||||
|
||||
return stream, err
|
||||
}
|
||||
|
||||
type response struct {
|
||||
stream Stream
|
||||
err error
|
||||
}
|
||||
|
||||
// get the retries
|
||||
retries := callOpts.Retries
|
||||
|
||||
// disable retries when using a proxy
|
||||
if _, _, ok := net.Proxy(request.Service(), callOpts.Address); ok {
|
||||
retries = 0
|
||||
}
|
||||
|
||||
ch := make(chan response, retries+1)
|
||||
|
||||
var grr error
|
||||
|
||||
for i := 0; i <= retries; i++ {
|
||||
go func(i int) {
|
||||
s, err := call(i)
|
||||
ch <- response{s, err}
|
||||
}(i)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, merrors.Timeout("go.micro.client", fmt.Sprintf("call timeout: %v", ctx.Err()))
|
||||
case rsp := <-ch:
|
||||
// if the call succeeded lets bail early
|
||||
if rsp.err == nil {
|
||||
return rsp.stream, nil
|
||||
}
|
||||
|
||||
retry, rerr := callOpts.Retry(ctx, request, i, rsp.err)
|
||||
if rerr != nil {
|
||||
return nil, rerr
|
||||
}
|
||||
|
||||
if !retry {
|
||||
return nil, rsp.err
|
||||
}
|
||||
|
||||
grr = rsp.err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, grr
|
||||
}
|
||||
|
||||
func (r *rpcClient) Publish(ctx context.Context, msg Message, opts ...PublishOption) error {
|
||||
options := PublishOptions{
|
||||
Context: context.Background(),
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
metadata, ok := metadata.FromContext(ctx)
|
||||
if !ok {
|
||||
metadata = make(map[string]string)
|
||||
}
|
||||
|
||||
id := uuid.New().String()
|
||||
metadata["Content-Type"] = msg.ContentType()
|
||||
metadata[headers.Message] = msg.Topic()
|
||||
metadata[headers.ID] = id
|
||||
|
||||
// set the topic
|
||||
topic := msg.Topic()
|
||||
|
||||
// get the exchange
|
||||
if len(options.Exchange) > 0 {
|
||||
topic = options.Exchange
|
||||
}
|
||||
|
||||
// encode message body
|
||||
cf, err := r.newCodec(msg.ContentType())
|
||||
if err != nil {
|
||||
return merrors.InternalServerError(packageID, err.Error())
|
||||
}
|
||||
|
||||
var body []byte
|
||||
|
||||
// passed in raw data
|
||||
if d, ok := msg.Payload().(*raw.Frame); ok {
|
||||
body = d.Data
|
||||
} else {
|
||||
b := buf.New(nil)
|
||||
|
||||
if err = cf(b).Write(&codec.Message{
|
||||
Target: topic,
|
||||
Type: codec.Event,
|
||||
Header: map[string]string{
|
||||
headers.ID: id,
|
||||
headers.Message: msg.Topic(),
|
||||
},
|
||||
}, msg.Payload()); err != nil {
|
||||
return merrors.InternalServerError(packageID, err.Error())
|
||||
}
|
||||
|
||||
// set the body
|
||||
body = b.Bytes()
|
||||
}
|
||||
|
||||
l, ok := r.once.Load().(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to cast to bool")
|
||||
}
|
||||
|
||||
if !l {
|
||||
if err = r.opts.Broker.Connect(); err != nil {
|
||||
return merrors.InternalServerError(packageID, err.Error())
|
||||
}
|
||||
|
||||
r.once.Store(true)
|
||||
}
|
||||
|
||||
return r.opts.Broker.Publish(topic, &broker.Message{
|
||||
Header: metadata,
|
||||
Body: body,
|
||||
}, broker.PublishContext(options.Context))
|
||||
}
|
||||
|
||||
func (r *rpcClient) NewMessage(topic string, message interface{}, opts ...MessageOption) Message {
|
||||
return newMessage(topic, message, r.opts.ContentType, opts...)
|
||||
}
|
||||
|
||||
func (r *rpcClient) NewRequest(service, method string, request interface{}, reqOpts ...RequestOption) Request {
|
||||
return newRequest(service, method, request, r.opts.ContentType, reqOpts...)
|
||||
}
|
||||
|
||||
func (r *rpcClient) String() string {
|
||||
return "mucp"
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
errs "errors"
|
||||
|
||||
"go-micro.dev/v4/codec"
|
||||
raw "go-micro.dev/v4/codec/bytes"
|
||||
"go-micro.dev/v4/codec/grpc"
|
||||
"go-micro.dev/v4/codec/json"
|
||||
"go-micro.dev/v4/codec/jsonrpc"
|
||||
"go-micro.dev/v4/codec/proto"
|
||||
"go-micro.dev/v4/codec/protorpc"
|
||||
"go-micro.dev/v4/errors"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/transport"
|
||||
"go-micro.dev/v4/transport/headers"
|
||||
)
|
||||
|
||||
const (
|
||||
lastStreamResponseError = "EOS"
|
||||
)
|
||||
|
||||
// serverError represents an error that has been returned from
|
||||
// the remote side of the RPC connection.
|
||||
type serverError string
|
||||
|
||||
func (e serverError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// errShutdown holds the specific error for closing/closed connections.
|
||||
var (
|
||||
errShutdown = errs.New("connection is shut down")
|
||||
)
|
||||
|
||||
type rpcCodec struct {
|
||||
client transport.Client
|
||||
codec codec.Codec
|
||||
|
||||
req *transport.Message
|
||||
buf *readWriteCloser
|
||||
|
||||
// signify if its a stream
|
||||
stream string
|
||||
}
|
||||
|
||||
type readWriteCloser struct {
|
||||
wbuf *bytes.Buffer
|
||||
rbuf *bytes.Buffer
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultContentType header.
|
||||
DefaultContentType = "application/json"
|
||||
|
||||
// DefaultCodecs map.
|
||||
DefaultCodecs = map[string]codec.NewCodec{
|
||||
"application/grpc": grpc.NewCodec,
|
||||
"application/grpc+json": grpc.NewCodec,
|
||||
"application/grpc+proto": grpc.NewCodec,
|
||||
"application/protobuf": proto.NewCodec,
|
||||
"application/json": json.NewCodec,
|
||||
"application/json-rpc": jsonrpc.NewCodec,
|
||||
"application/proto-rpc": protorpc.NewCodec,
|
||||
"application/octet-stream": raw.NewCodec,
|
||||
}
|
||||
|
||||
// TODO: remove legacy codec list.
|
||||
defaultCodecs = map[string]codec.NewCodec{
|
||||
"application/json": jsonrpc.NewCodec,
|
||||
"application/json-rpc": jsonrpc.NewCodec,
|
||||
"application/protobuf": protorpc.NewCodec,
|
||||
"application/proto-rpc": protorpc.NewCodec,
|
||||
"application/octet-stream": protorpc.NewCodec,
|
||||
}
|
||||
)
|
||||
|
||||
func (rwc *readWriteCloser) Read(p []byte) (n int, err error) {
|
||||
return rwc.rbuf.Read(p)
|
||||
}
|
||||
|
||||
func (rwc *readWriteCloser) Write(p []byte) (n int, err error) {
|
||||
return rwc.wbuf.Write(p)
|
||||
}
|
||||
|
||||
func (rwc *readWriteCloser) Close() error {
|
||||
rwc.rbuf.Reset()
|
||||
rwc.wbuf.Reset()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getHeaders(m *codec.Message) {
|
||||
set := func(v, hdr string) string {
|
||||
if len(v) > 0 {
|
||||
return v
|
||||
}
|
||||
|
||||
return m.Header[hdr]
|
||||
}
|
||||
|
||||
// check error in header
|
||||
m.Error = set(m.Error, headers.Error)
|
||||
|
||||
// check endpoint in header
|
||||
m.Endpoint = set(m.Endpoint, headers.Endpoint)
|
||||
|
||||
// check method in header
|
||||
m.Method = set(m.Method, headers.Method)
|
||||
|
||||
// set the request id
|
||||
m.Id = set(m.Id, headers.ID)
|
||||
}
|
||||
|
||||
func setHeaders(m *codec.Message, stream string) {
|
||||
set := func(hdr, v string) {
|
||||
if len(v) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
m.Header[hdr] = v
|
||||
}
|
||||
|
||||
set(headers.ID, m.Id)
|
||||
set(headers.Request, m.Target)
|
||||
set(headers.Method, m.Method)
|
||||
set(headers.Endpoint, m.Endpoint)
|
||||
set(headers.Error, m.Error)
|
||||
|
||||
if len(stream) > 0 {
|
||||
set(headers.Stream, stream)
|
||||
}
|
||||
}
|
||||
|
||||
// setupProtocol sets up the old protocol.
|
||||
func setupProtocol(msg *transport.Message, node *registry.Node) codec.NewCodec {
|
||||
protocol := node.Metadata["protocol"]
|
||||
|
||||
// got protocol
|
||||
if len(protocol) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// processing topic publishing
|
||||
if len(msg.Header[headers.Message]) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// no protocol use old codecs
|
||||
switch msg.Header["Content-Type"] {
|
||||
case "application/json":
|
||||
msg.Header["Content-Type"] = "application/json-rpc"
|
||||
case "application/protobuf":
|
||||
msg.Header["Content-Type"] = "application/proto-rpc"
|
||||
}
|
||||
|
||||
return defaultCodecs[msg.Header["Content-Type"]]
|
||||
}
|
||||
|
||||
func newRPCCodec(req *transport.Message, client transport.Client, c codec.NewCodec, stream string) codec.Codec {
|
||||
rwc := &readWriteCloser{
|
||||
wbuf: bytes.NewBuffer(nil),
|
||||
rbuf: bytes.NewBuffer(nil),
|
||||
}
|
||||
|
||||
return &rpcCodec{
|
||||
buf: rwc,
|
||||
client: client,
|
||||
codec: c(rwc),
|
||||
req: req,
|
||||
stream: stream,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *rpcCodec) Write(message *codec.Message, body interface{}) error {
|
||||
c.buf.wbuf.Reset()
|
||||
|
||||
// create header
|
||||
if message.Header == nil {
|
||||
message.Header = map[string]string{}
|
||||
}
|
||||
|
||||
// copy original header
|
||||
for k, v := range c.req.Header {
|
||||
message.Header[k] = v
|
||||
}
|
||||
|
||||
// set the mucp headers
|
||||
setHeaders(message, c.stream)
|
||||
|
||||
// if body is bytes Frame don't encode
|
||||
if body != nil {
|
||||
if b, ok := body.(*raw.Frame); ok {
|
||||
// set body
|
||||
message.Body = b.Data
|
||||
} else {
|
||||
// write to codec
|
||||
if err := c.codec.Write(message, body); err != nil {
|
||||
return errors.InternalServerError("go.micro.client.codec", err.Error())
|
||||
}
|
||||
// set body
|
||||
message.Body = c.buf.wbuf.Bytes()
|
||||
}
|
||||
}
|
||||
|
||||
// create new transport message
|
||||
msg := transport.Message{
|
||||
Header: message.Header,
|
||||
Body: message.Body,
|
||||
}
|
||||
|
||||
// send the request
|
||||
if err := c.client.Send(&msg); err != nil {
|
||||
return errors.InternalServerError("go.micro.client.transport", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *rpcCodec) ReadHeader(msg *codec.Message, r codec.MessageType) error {
|
||||
var tm transport.Message
|
||||
|
||||
// read message from transport
|
||||
if err := c.client.Recv(&tm); err != nil {
|
||||
return errors.InternalServerError("go.micro.client.transport", err.Error())
|
||||
}
|
||||
|
||||
c.buf.rbuf.Reset()
|
||||
c.buf.rbuf.Write(tm.Body)
|
||||
|
||||
// set headers from transport
|
||||
msg.Header = tm.Header
|
||||
|
||||
// read header
|
||||
err := c.codec.ReadHeader(msg, r)
|
||||
|
||||
// get headers
|
||||
getHeaders(msg)
|
||||
|
||||
// return header error
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client.codec", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *rpcCodec) ReadBody(b interface{}) error {
|
||||
// read body
|
||||
// read raw data
|
||||
if v, ok := b.(*raw.Frame); ok {
|
||||
v.Data = c.buf.rbuf.Bytes()
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.codec.ReadBody(b); err != nil {
|
||||
return errors.InternalServerError("go.micro.client.codec", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *rpcCodec) Close() error {
|
||||
if err := c.buf.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.codec.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.client.Close(); err != nil {
|
||||
return errors.InternalServerError("go.micro.client.transport", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *rpcCodec) String() string {
|
||||
return "rpc"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package client
|
||||
|
||||
type message struct {
|
||||
payload interface{}
|
||||
topic string
|
||||
contentType string
|
||||
}
|
||||
|
||||
func newMessage(topic string, payload interface{}, contentType string, opts ...MessageOption) Message {
|
||||
var options MessageOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if len(options.ContentType) > 0 {
|
||||
contentType = options.ContentType
|
||||
}
|
||||
|
||||
return &message{
|
||||
payload: payload,
|
||||
topic: topic,
|
||||
contentType: contentType,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *message) ContentType() string {
|
||||
return m.contentType
|
||||
}
|
||||
|
||||
func (m *message) Topic() string {
|
||||
return m.topic
|
||||
}
|
||||
|
||||
func (m *message) Payload() interface{} {
|
||||
return m.payload
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
type rpcRequest struct {
|
||||
opts RequestOptions
|
||||
codec codec.Codec
|
||||
body interface{}
|
||||
service string
|
||||
method string
|
||||
endpoint string
|
||||
contentType string
|
||||
}
|
||||
|
||||
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 &rpcRequest{
|
||||
service: service,
|
||||
method: endpoint,
|
||||
endpoint: endpoint,
|
||||
body: request,
|
||||
contentType: contentType,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rpcRequest) ContentType() string {
|
||||
return r.contentType
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Service() string {
|
||||
return r.service
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Method() string {
|
||||
return r.method
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Endpoint() string {
|
||||
return r.endpoint
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Body() interface{} {
|
||||
return r.body
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Codec() codec.Writer {
|
||||
return r.codec
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Stream() bool {
|
||||
return r.opts.Stream
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"go-micro.dev/v4/codec"
|
||||
"go-micro.dev/v4/transport"
|
||||
)
|
||||
|
||||
type rpcResponse struct {
|
||||
socket transport.Socket
|
||||
codec codec.Codec
|
||||
header map[string]string
|
||||
body []byte
|
||||
}
|
||||
|
||||
func (r *rpcResponse) Codec() codec.Reader {
|
||||
return r.codec
|
||||
}
|
||||
|
||||
func (r *rpcResponse) Header() map[string]string {
|
||||
return r.header
|
||||
}
|
||||
|
||||
func (r *rpcResponse) Read() ([]byte, error) {
|
||||
var msg transport.Message
|
||||
|
||||
if err := r.socket.Recv(&msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set internals
|
||||
r.header = msg.Header
|
||||
r.body = msg.Body
|
||||
|
||||
return msg.Body, nil
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
// Implements the streamer interface.
|
||||
type rpcStream struct {
|
||||
err error
|
||||
request Request
|
||||
response Response
|
||||
codec codec.Codec
|
||||
context context.Context
|
||||
|
||||
closed chan bool
|
||||
|
||||
// release releases the connection back to the pool
|
||||
release func(err error)
|
||||
id string
|
||||
sync.RWMutex
|
||||
// Indicates whether connection should be closed directly.
|
||||
close bool
|
||||
|
||||
// signal whether we should send EOS
|
||||
sendEOS bool
|
||||
}
|
||||
|
||||
func (r *rpcStream) isClosed() bool {
|
||||
select {
|
||||
case <-r.closed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rpcStream) Context() context.Context {
|
||||
return r.context
|
||||
}
|
||||
|
||||
func (r *rpcStream) Request() Request {
|
||||
return r.request
|
||||
}
|
||||
|
||||
func (r *rpcStream) Response() Response {
|
||||
return r.response
|
||||
}
|
||||
|
||||
func (r *rpcStream) Send(msg interface{}) error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if r.isClosed() {
|
||||
r.err = errShutdown
|
||||
return errShutdown
|
||||
}
|
||||
|
||||
req := codec.Message{
|
||||
Id: r.id,
|
||||
Target: r.request.Service(),
|
||||
Method: r.request.Method(),
|
||||
Endpoint: r.request.Endpoint(),
|
||||
Type: codec.Request,
|
||||
}
|
||||
|
||||
if err := r.codec.Write(&req, msg); err != nil {
|
||||
r.err = err
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rpcStream) Recv(msg interface{}) error {
|
||||
r.Lock()
|
||||
|
||||
if r.isClosed() {
|
||||
r.err = errShutdown
|
||||
r.Unlock()
|
||||
|
||||
return errShutdown
|
||||
}
|
||||
|
||||
var resp codec.Message
|
||||
|
||||
r.Unlock()
|
||||
err := r.codec.ReadHeader(&resp, codec.Response)
|
||||
r.Lock()
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) && !r.isClosed() {
|
||||
r.err = io.ErrUnexpectedEOF
|
||||
r.Unlock()
|
||||
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
r.err = err
|
||||
|
||||
r.Unlock()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(resp.Error) > 0:
|
||||
// We've got an error response. Give this to the request;
|
||||
// any subsequent requests will get the ReadResponseBody
|
||||
// error if there is one.
|
||||
if resp.Error != lastStreamResponseError {
|
||||
r.err = serverError(resp.Error)
|
||||
} else {
|
||||
r.err = io.EOF
|
||||
}
|
||||
r.Unlock()
|
||||
err = r.codec.ReadBody(nil)
|
||||
r.Lock()
|
||||
if err != nil {
|
||||
r.err = err
|
||||
}
|
||||
default:
|
||||
r.Unlock()
|
||||
err = r.codec.ReadBody(msg)
|
||||
r.Lock()
|
||||
if err != nil {
|
||||
r.err = err
|
||||
}
|
||||
}
|
||||
|
||||
defer r.Unlock()
|
||||
|
||||
return r.err
|
||||
}
|
||||
|
||||
func (r *rpcStream) Error() error {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
|
||||
return r.err
|
||||
}
|
||||
|
||||
func (r *rpcStream) CloseSend() error {
|
||||
return errors.New("streamer not implemented")
|
||||
}
|
||||
|
||||
func (r *rpcStream) Close() error {
|
||||
r.Lock()
|
||||
|
||||
select {
|
||||
case <-r.closed:
|
||||
r.Unlock()
|
||||
return nil
|
||||
default:
|
||||
close(r.closed)
|
||||
r.Unlock()
|
||||
|
||||
// send the end of stream message
|
||||
if r.sendEOS {
|
||||
// no need to check for error
|
||||
//nolint:errcheck,gosec
|
||||
r.codec.Write(&codec.Message{
|
||||
Id: r.id,
|
||||
Target: r.request.Service(),
|
||||
Method: r.request.Method(),
|
||||
Endpoint: r.request.Endpoint(),
|
||||
Type: codec.Error,
|
||||
Error: lastStreamResponseError,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
err := r.codec.Close()
|
||||
|
||||
rerr := r.Error()
|
||||
if r.close && rerr == nil {
|
||||
rerr = errors.New("connection header set to close")
|
||||
}
|
||||
// release the connection
|
||||
r.release(rerr)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/registry"
|
||||
)
|
||||
|
||||
// CallFunc represents the individual call func.
|
||||
type CallFunc func(ctx context.Context, node *registry.Node, req Request, rsp interface{}, opts CallOptions) error
|
||||
|
||||
// CallWrapper is a low level wrapper for the CallFunc.
|
||||
type CallWrapper func(CallFunc) CallFunc
|
||||
|
||||
// Wrapper wraps a client and returns a client.
|
||||
type Wrapper func(Client) Client
|
||||
|
||||
// StreamWrapper wraps a Stream and returns the equivalent.
|
||||
type StreamWrapper func(Stream) Stream
|
||||
@@ -0,0 +1,71 @@
|
||||
// Package bytes provides a bytes codec which does not encode or decode anything
|
||||
package bytes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
type Codec struct {
|
||||
Conn io.ReadWriteCloser
|
||||
}
|
||||
|
||||
// Frame gives us the ability to define raw data to send over the pipes.
|
||||
type Frame struct {
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (c *Codec) ReadHeader(m *codec.Message, t codec.MessageType) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Codec) ReadBody(b interface{}) error {
|
||||
// read bytes
|
||||
buf, err := io.ReadAll(c.Conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch v := b.(type) {
|
||||
case *[]byte:
|
||||
*v = buf
|
||||
case *Frame:
|
||||
v.Data = buf
|
||||
default:
|
||||
return fmt.Errorf("failed to read body: %v is not type of *[]byte", b)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Codec) Write(m *codec.Message, b interface{}) error {
|
||||
var v []byte
|
||||
switch vb := b.(type) {
|
||||
case *Frame:
|
||||
v = vb.Data
|
||||
case *[]byte:
|
||||
v = *vb
|
||||
case []byte:
|
||||
v = vb
|
||||
default:
|
||||
return fmt.Errorf("failed to write: %v is not type of *[]byte or []byte", b)
|
||||
}
|
||||
_, err := c.Conn.Write(v)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Codec) Close() error {
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *Codec) String() string {
|
||||
return "bytes"
|
||||
}
|
||||
|
||||
func NewCodec(c io.ReadWriteCloser) codec.Codec {
|
||||
return &Codec{
|
||||
Conn: c,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package bytes
|
||||
|
||||
import (
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
type Marshaler struct{}
|
||||
|
||||
type Message struct {
|
||||
Header map[string]string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (n Marshaler) Marshal(v interface{}) ([]byte, error) {
|
||||
switch ve := v.(type) {
|
||||
case *[]byte:
|
||||
return *ve, nil
|
||||
case []byte:
|
||||
return ve, nil
|
||||
case *Message:
|
||||
return ve.Body, nil
|
||||
}
|
||||
return nil, codec.ErrInvalidMessage
|
||||
}
|
||||
|
||||
func (n Marshaler) Unmarshal(d []byte, v interface{}) error {
|
||||
switch ve := v.(type) {
|
||||
case *[]byte:
|
||||
*ve = d
|
||||
return nil
|
||||
case *Message:
|
||||
ve.Body = d
|
||||
return nil
|
||||
}
|
||||
return codec.ErrInvalidMessage
|
||||
}
|
||||
|
||||
func (n Marshaler) String() string {
|
||||
return "bytes"
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
// Package codec is an interface for encoding messages
|
||||
package codec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
Error MessageType = iota
|
||||
Request
|
||||
Response
|
||||
Event
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidMessage = errors.New("invalid message")
|
||||
)
|
||||
|
||||
type MessageType int
|
||||
|
||||
// Takes in a connection/buffer and returns a new Codec.
|
||||
type NewCodec func(io.ReadWriteCloser) Codec
|
||||
|
||||
// Codec encodes/decodes various types of messages used within go-micro.
|
||||
// ReadHeader and ReadBody are called in pairs to read requests/responses
|
||||
// from the connection. Close is called when finished with the
|
||||
// connection. ReadBody may be called with a nil argument to force the
|
||||
// body to be read and discarded.
|
||||
type Codec interface {
|
||||
Reader
|
||||
Writer
|
||||
Close() error
|
||||
String() string
|
||||
}
|
||||
|
||||
type Reader interface {
|
||||
ReadHeader(*Message, MessageType) error
|
||||
ReadBody(interface{}) error
|
||||
}
|
||||
|
||||
type Writer interface {
|
||||
Write(*Message, interface{}) error
|
||||
}
|
||||
|
||||
// Marshaler is a simple encoding interface used for the broker/transport
|
||||
// where headers are not supported by the underlying implementation.
|
||||
type Marshaler interface {
|
||||
Marshal(interface{}) ([]byte, error)
|
||||
Unmarshal([]byte, interface{}) error
|
||||
String() string
|
||||
}
|
||||
|
||||
// Message represents detailed information about
|
||||
// the communication, likely followed by the body.
|
||||
// In the case of an error, body may be nil.
|
||||
type Message struct {
|
||||
|
||||
// The values read from the socket
|
||||
Header map[string]string
|
||||
Id string
|
||||
Target string
|
||||
Method string
|
||||
Endpoint string
|
||||
Error string
|
||||
|
||||
Body []byte
|
||||
Type MessageType
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
// Package grpc provides a grpc codec
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"go-micro.dev/v4/codec"
|
||||
"go-micro.dev/v4/transport/headers"
|
||||
)
|
||||
|
||||
type Codec struct {
|
||||
Conn io.ReadWriteCloser
|
||||
ContentType string
|
||||
}
|
||||
|
||||
func (c *Codec) ReadHeader(m *codec.Message, t codec.MessageType) error {
|
||||
if ct := m.Header["Content-Type"]; len(ct) > 0 {
|
||||
c.ContentType = ct
|
||||
}
|
||||
|
||||
if ct := m.Header["content-type"]; len(ct) > 0 {
|
||||
c.ContentType = ct
|
||||
}
|
||||
|
||||
// service method
|
||||
path := m.Header[":path"]
|
||||
if len(path) == 0 || path[0] != '/' {
|
||||
m.Target = m.Header[headers.Request]
|
||||
m.Endpoint = m.Header[headers.Endpoint]
|
||||
} else {
|
||||
// [ , a.package.Foo, Bar]
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) != 3 {
|
||||
return errors.New("Unknown request path")
|
||||
}
|
||||
service := strings.Split(parts[1], ".")
|
||||
m.Endpoint = strings.Join([]string{service[len(service)-1], parts[2]}, ".")
|
||||
m.Target = strings.Join(service[:len(service)-1], ".")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Codec) ReadBody(b interface{}) error {
|
||||
// no body
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, buf, err := decode(c.Conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch c.ContentType {
|
||||
case "application/grpc+json":
|
||||
return json.Unmarshal(buf, b)
|
||||
case "application/grpc+proto", "application/grpc":
|
||||
return proto.Unmarshal(buf, b.(proto.Message))
|
||||
}
|
||||
|
||||
return errors.New("Unsupported Content-Type")
|
||||
}
|
||||
|
||||
func (c *Codec) Write(m *codec.Message, b interface{}) error {
|
||||
var buf []byte
|
||||
var err error
|
||||
|
||||
if ct := m.Header["Content-Type"]; len(ct) > 0 {
|
||||
c.ContentType = ct
|
||||
}
|
||||
|
||||
if ct := m.Header["content-type"]; len(ct) > 0 {
|
||||
c.ContentType = ct
|
||||
}
|
||||
|
||||
switch m.Type {
|
||||
case codec.Request:
|
||||
parts := strings.Split(m.Endpoint, ".")
|
||||
m.Header[":method"] = "POST"
|
||||
m.Header[":path"] = fmt.Sprintf("/%s.%s/%s", m.Target, parts[0], parts[1])
|
||||
m.Header[":proto"] = "HTTP/2.0"
|
||||
m.Header["te"] = "trailers"
|
||||
m.Header["user-agent"] = "grpc-go/1.0.0"
|
||||
m.Header[":authority"] = m.Target
|
||||
m.Header["content-type"] = c.ContentType
|
||||
case codec.Response:
|
||||
m.Header["Trailer"] = "grpc-status" // , grpc-message"
|
||||
m.Header["content-type"] = c.ContentType
|
||||
m.Header[":status"] = "200"
|
||||
m.Header["grpc-status"] = "0"
|
||||
// m.Header["grpc-message"] = ""
|
||||
case codec.Error:
|
||||
m.Header["Trailer"] = "grpc-status, grpc-message"
|
||||
// micro end of stream
|
||||
if m.Error == "EOS" {
|
||||
m.Header["grpc-status"] = "0"
|
||||
} else {
|
||||
m.Header["grpc-message"] = m.Error
|
||||
m.Header["grpc-status"] = "13"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshal content
|
||||
switch c.ContentType {
|
||||
case "application/grpc+json":
|
||||
buf, err = json.Marshal(b)
|
||||
case "application/grpc+proto", "application/grpc":
|
||||
pb, ok := b.(proto.Message)
|
||||
if ok {
|
||||
buf, err = proto.Marshal(pb)
|
||||
}
|
||||
default:
|
||||
err = errors.New("Unsupported Content-Type")
|
||||
}
|
||||
// check error
|
||||
if err != nil {
|
||||
m.Header["grpc-status"] = "8"
|
||||
m.Header["grpc-message"] = err.Error()
|
||||
return err
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return encode(0, buf, c.Conn)
|
||||
}
|
||||
|
||||
func (c *Codec) Close() error {
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *Codec) String() string {
|
||||
return "grpc"
|
||||
}
|
||||
|
||||
func NewCodec(c io.ReadWriteCloser) codec.Codec {
|
||||
return &Codec{
|
||||
Conn: c,
|
||||
ContentType: "application/grpc",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
var (
|
||||
MaxMessageSize = 1024 * 1024 * 4 // 4Mb
|
||||
maxInt = int(^uint(0) >> 1)
|
||||
)
|
||||
|
||||
func decode(r io.Reader) (uint8, []byte, error) {
|
||||
header := make([]byte, 5)
|
||||
|
||||
// read the header
|
||||
if _, err := r.Read(header); err != nil {
|
||||
return uint8(0), nil, err
|
||||
}
|
||||
|
||||
// get encoding format e.g compressed
|
||||
cf := uint8(header[0])
|
||||
|
||||
// get message length
|
||||
length := binary.BigEndian.Uint32(header[1:])
|
||||
|
||||
// no encoding format
|
||||
if length == 0 {
|
||||
return cf, nil, nil
|
||||
}
|
||||
|
||||
//
|
||||
if int64(length) > int64(maxInt) {
|
||||
return cf, nil, fmt.Errorf("grpc: received message larger than max length allowed on current machine (%d vs. %d)", length, maxInt)
|
||||
}
|
||||
if int(length) > MaxMessageSize {
|
||||
return cf, nil, fmt.Errorf("grpc: received message larger than max (%d vs. %d)", length, MaxMessageSize)
|
||||
}
|
||||
|
||||
msg := make([]byte, int(length))
|
||||
|
||||
if _, err := r.Read(msg); err != nil {
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return cf, nil, err
|
||||
}
|
||||
|
||||
return cf, msg, nil
|
||||
}
|
||||
|
||||
func encode(cf uint8, buf []byte, w io.Writer) error {
|
||||
header := make([]byte, 5)
|
||||
|
||||
// set compression
|
||||
header[0] = byte(cf)
|
||||
|
||||
// write length as header
|
||||
binary.BigEndian.PutUint32(header[1:], uint32(len(buf)))
|
||||
|
||||
// read the header
|
||||
if _, err := w.Write(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write the buffer
|
||||
_, err := w.Write(buf)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Package json provides a json codec
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
type Codec struct {
|
||||
Conn io.ReadWriteCloser
|
||||
Encoder *json.Encoder
|
||||
Decoder *json.Decoder
|
||||
}
|
||||
|
||||
func (c *Codec) ReadHeader(m *codec.Message, t codec.MessageType) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Codec) ReadBody(b interface{}) error {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
if pb, ok := b.(proto.Message); ok {
|
||||
return jsonpb.UnmarshalNext(c.Decoder, pb)
|
||||
}
|
||||
return c.Decoder.Decode(b)
|
||||
}
|
||||
|
||||
func (c *Codec) Write(m *codec.Message, b interface{}) error {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return c.Encoder.Encode(b)
|
||||
}
|
||||
|
||||
func (c *Codec) Close() error {
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *Codec) String() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
func NewCodec(c io.ReadWriteCloser) codec.Codec {
|
||||
return &Codec{
|
||||
Conn: c,
|
||||
Decoder: json.NewDecoder(c),
|
||||
Encoder: json.NewEncoder(c),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/oxtoacart/bpool"
|
||||
)
|
||||
|
||||
var jsonpbMarshaler = &jsonpb.Marshaler{}
|
||||
|
||||
// create buffer pool with 16 instances each preallocated with 256 bytes.
|
||||
var bufferPool = bpool.NewSizedBufferPool(16, 256)
|
||||
|
||||
type Marshaler struct{}
|
||||
|
||||
func (j Marshaler) Marshal(v interface{}) ([]byte, error) {
|
||||
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 (j Marshaler) Unmarshal(d []byte, v interface{}) error {
|
||||
if pb, ok := v.(proto.Message); ok {
|
||||
return jsonpb.Unmarshal(bytes.NewReader(d), pb)
|
||||
}
|
||||
return json.Unmarshal(d, v)
|
||||
}
|
||||
|
||||
func (j Marshaler) String() string {
|
||||
return "json"
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package jsonrpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
type clientCodec struct {
|
||||
|
||||
// temporary work space
|
||||
req clientRequest
|
||||
resp clientResponse
|
||||
|
||||
c io.Closer
|
||||
|
||||
dec *json.Decoder // for reading JSON values
|
||||
enc *json.Encoder // for writing JSON values
|
||||
pending map[interface{}]string
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
type clientRequest struct {
|
||||
Params [1]interface{} `json:"params"`
|
||||
ID interface{} `json:"id"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
type clientResponse struct {
|
||||
ID interface{} `json:"id"`
|
||||
Result *json.RawMessage `json:"result"`
|
||||
Error interface{} `json:"error"`
|
||||
}
|
||||
|
||||
func newClientCodec(conn io.ReadWriteCloser) *clientCodec {
|
||||
return &clientCodec{
|
||||
dec: json.NewDecoder(conn),
|
||||
enc: json.NewEncoder(conn),
|
||||
c: conn,
|
||||
pending: make(map[interface{}]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clientCodec) Write(m *codec.Message, b interface{}) error {
|
||||
c.Lock()
|
||||
c.pending[m.Id] = m.Method
|
||||
c.Unlock()
|
||||
c.req.Method = m.Method
|
||||
c.req.Params[0] = b
|
||||
c.req.ID = m.Id
|
||||
return c.enc.Encode(&c.req)
|
||||
}
|
||||
|
||||
func (r *clientResponse) reset() {
|
||||
r.ID = 0
|
||||
r.Result = nil
|
||||
r.Error = nil
|
||||
}
|
||||
|
||||
func (c *clientCodec) ReadHeader(m *codec.Message) error {
|
||||
c.resp.reset()
|
||||
if err := c.dec.Decode(&c.resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
m.Method = c.pending[c.resp.ID]
|
||||
delete(c.pending, c.resp.ID)
|
||||
c.Unlock()
|
||||
|
||||
m.Error = ""
|
||||
m.Id = fmt.Sprintf("%v", c.resp.ID)
|
||||
if c.resp.Error != nil {
|
||||
x, ok := c.resp.Error.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid error %v", c.resp.Error)
|
||||
}
|
||||
if x == "" {
|
||||
x = "unspecified error"
|
||||
}
|
||||
m.Error = x
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clientCodec) ReadBody(x interface{}) error {
|
||||
if x == nil || c.resp.Result == nil {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(*c.resp.Result, x)
|
||||
}
|
||||
|
||||
func (c *clientCodec) Close() error {
|
||||
return c.c.Close()
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Package jsonrpc provides a json-rpc 1.0 codec
|
||||
package jsonrpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
type jsonCodec struct {
|
||||
rwc io.ReadWriteCloser
|
||||
buf *bytes.Buffer
|
||||
c *clientCodec
|
||||
s *serverCodec
|
||||
mt codec.MessageType
|
||||
}
|
||||
|
||||
func (j *jsonCodec) Close() error {
|
||||
j.buf.Reset()
|
||||
return j.rwc.Close()
|
||||
}
|
||||
|
||||
func (j *jsonCodec) String() string {
|
||||
return "json-rpc"
|
||||
}
|
||||
|
||||
func (j *jsonCodec) Write(m *codec.Message, b interface{}) error {
|
||||
switch m.Type {
|
||||
case codec.Request:
|
||||
return j.c.Write(m, b)
|
||||
case codec.Response, codec.Error:
|
||||
return j.s.Write(m, b)
|
||||
case codec.Event:
|
||||
data, err := json.Marshal(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = j.rwc.Write(data)
|
||||
return err
|
||||
default:
|
||||
return fmt.Errorf("Unrecognized message type: %v", m.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (j *jsonCodec) ReadHeader(m *codec.Message, mt codec.MessageType) error {
|
||||
j.buf.Reset()
|
||||
j.mt = mt
|
||||
|
||||
switch mt {
|
||||
case codec.Request:
|
||||
return j.s.ReadHeader(m)
|
||||
case codec.Response:
|
||||
return j.c.ReadHeader(m)
|
||||
case codec.Event:
|
||||
_, err := io.Copy(j.buf, j.rwc)
|
||||
return err
|
||||
default:
|
||||
return fmt.Errorf("Unrecognized message type: %v", mt)
|
||||
}
|
||||
}
|
||||
|
||||
func (j *jsonCodec) ReadBody(b interface{}) error {
|
||||
switch j.mt {
|
||||
case codec.Request:
|
||||
return j.s.ReadBody(b)
|
||||
case codec.Response:
|
||||
return j.c.ReadBody(b)
|
||||
case codec.Event:
|
||||
if b != nil {
|
||||
return json.Unmarshal(j.buf.Bytes(), b)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Unrecognized message type: %v", j.mt)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewCodec(rwc io.ReadWriteCloser) codec.Codec {
|
||||
return &jsonCodec{
|
||||
buf: bytes.NewBuffer(nil),
|
||||
rwc: rwc,
|
||||
c: newClientCodec(rwc),
|
||||
s: newServerCodec(rwc),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package jsonrpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
type serverCodec struct {
|
||||
dec *json.Decoder // for reading JSON values
|
||||
enc *json.Encoder // for writing JSON values
|
||||
c io.Closer
|
||||
|
||||
// temporary work space
|
||||
req serverRequest
|
||||
resp serverResponse
|
||||
}
|
||||
|
||||
type serverRequest struct {
|
||||
ID interface{} `json:"id"`
|
||||
Params *json.RawMessage `json:"params"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
type serverResponse struct {
|
||||
ID interface{} `json:"id"`
|
||||
Result interface{} `json:"result"`
|
||||
Error interface{} `json:"error"`
|
||||
}
|
||||
|
||||
func newServerCodec(conn io.ReadWriteCloser) *serverCodec {
|
||||
return &serverCodec{
|
||||
dec: json.NewDecoder(conn),
|
||||
enc: json.NewEncoder(conn),
|
||||
c: conn,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *serverRequest) reset() {
|
||||
r.Method = ""
|
||||
if r.Params != nil {
|
||||
*r.Params = (*r.Params)[0:0]
|
||||
}
|
||||
if r.ID != nil {
|
||||
r.ID = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *serverCodec) ReadHeader(m *codec.Message) error {
|
||||
c.req.reset()
|
||||
if err := c.dec.Decode(&c.req); err != nil {
|
||||
return err
|
||||
}
|
||||
m.Method = c.req.Method
|
||||
m.Id = fmt.Sprintf("%v", c.req.ID)
|
||||
c.req.ID = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *serverCodec) ReadBody(x interface{}) error {
|
||||
if x == nil {
|
||||
return nil
|
||||
}
|
||||
var params [1]interface{}
|
||||
params[0] = x
|
||||
return json.Unmarshal(*c.req.Params, ¶ms)
|
||||
}
|
||||
|
||||
var null = json.RawMessage([]byte("null"))
|
||||
|
||||
func (c *serverCodec) Write(m *codec.Message, x interface{}) error {
|
||||
var resp serverResponse
|
||||
resp.ID = m.Id
|
||||
resp.Result = x
|
||||
if m.Error == "" {
|
||||
resp.Error = nil
|
||||
} else {
|
||||
resp.Error = m.Error
|
||||
}
|
||||
return c.enc.Encode(resp)
|
||||
}
|
||||
|
||||
func (c *serverCodec) Close() error {
|
||||
return c.c.Close()
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/oxtoacart/bpool"
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
// create buffer pool with 16 instances each preallocated with 256 bytes.
|
||||
var bufferPool = bpool.NewSizedBufferPool(16, 256)
|
||||
|
||||
type Marshaler struct{}
|
||||
|
||||
func (Marshaler) Marshal(v interface{}) ([]byte, error) {
|
||||
pb, ok := v.(proto.Message)
|
||||
if !ok {
|
||||
return nil, codec.ErrInvalidMessage
|
||||
}
|
||||
|
||||
// looks not good, but allows to reuse underlining bytes
|
||||
buf := bufferPool.Get()
|
||||
pbuf := proto.NewBuffer(buf.Bytes())
|
||||
defer func() {
|
||||
bufferPool.Put(bytes.NewBuffer(pbuf.Bytes()))
|
||||
}()
|
||||
|
||||
if err := pbuf.Marshal(pb); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pbuf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (Marshaler) Unmarshal(data []byte, v interface{}) error {
|
||||
pb, ok := v.(proto.Message)
|
||||
if !ok {
|
||||
return codec.ErrInvalidMessage
|
||||
}
|
||||
|
||||
return proto.Unmarshal(data, pb)
|
||||
}
|
||||
|
||||
func (Marshaler) String() string {
|
||||
return "proto"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package proto
|
||||
|
||||
type Message struct {
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (m *Message) MarshalJSON() ([]byte, error) {
|
||||
return m.Data, nil
|
||||
}
|
||||
|
||||
func (m *Message) UnmarshalJSON(data []byte) error {
|
||||
m.Data = data
|
||||
return nil
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Package proto provides a proto codec
|
||||
package proto
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
type Codec struct {
|
||||
Conn io.ReadWriteCloser
|
||||
}
|
||||
|
||||
func (c *Codec) ReadHeader(m *codec.Message, t codec.MessageType) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Codec) ReadBody(b interface{}) error {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
buf, err := io.ReadAll(c.Conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m, ok := b.(proto.Message)
|
||||
if !ok {
|
||||
return codec.ErrInvalidMessage
|
||||
}
|
||||
return proto.Unmarshal(buf, m)
|
||||
}
|
||||
|
||||
func (c *Codec) Write(m *codec.Message, b interface{}) error {
|
||||
if b == nil {
|
||||
// Nothing to write
|
||||
return nil
|
||||
}
|
||||
p, ok := b.(proto.Message)
|
||||
if !ok {
|
||||
return codec.ErrInvalidMessage
|
||||
}
|
||||
buf, err := proto.Marshal(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.Conn.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Codec) Close() error {
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *Codec) String() string {
|
||||
return "proto"
|
||||
}
|
||||
|
||||
func NewCodec(c io.ReadWriteCloser) codec.Codec {
|
||||
return &Codec{
|
||||
Conn: c,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// source: codec/protorpc/envelope.proto
|
||||
|
||||
package protorpc
|
||||
|
||||
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 Request struct {
|
||||
ServiceMethod string `protobuf:"bytes,1,opt,name=service_method,json=serviceMethod,proto3" json:"service_method,omitempty"`
|
||||
Seq uint64 `protobuf:"fixed64,2,opt,name=seq,proto3" json:"seq,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_12fd17ed7ee86a33, []int{0}
|
||||
}
|
||||
|
||||
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) GetServiceMethod() string {
|
||||
if m != nil {
|
||||
return m.ServiceMethod
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Request) GetSeq() uint64 {
|
||||
if m != nil {
|
||||
return m.Seq
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
ServiceMethod string `protobuf:"bytes,1,opt,name=service_method,json=serviceMethod,proto3" json:"service_method,omitempty"`
|
||||
Seq uint64 `protobuf:"fixed64,2,opt,name=seq,proto3" json:"seq,omitempty"`
|
||||
Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,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_12fd17ed7ee86a33, []int{1}
|
||||
}
|
||||
|
||||
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) GetServiceMethod() string {
|
||||
if m != nil {
|
||||
return m.ServiceMethod
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Response) GetSeq() uint64 {
|
||||
if m != nil {
|
||||
return m.Seq
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *Response) GetError() string {
|
||||
if m != nil {
|
||||
return m.Error
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*Request)(nil), "protorpc.Request")
|
||||
proto.RegisterType((*Response)(nil), "protorpc.Response")
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("codec/protorpc/envelope.proto", fileDescriptor_12fd17ed7ee86a33) }
|
||||
|
||||
var fileDescriptor_12fd17ed7ee86a33 = []byte{
|
||||
// 148 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4d, 0xce, 0x4f, 0x49,
|
||||
0x4d, 0xd6, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0x2f, 0x2a, 0x48, 0xd6, 0x4f, 0xcd, 0x2b, 0x4b, 0xcd,
|
||||
0xc9, 0x2f, 0x48, 0xd5, 0x03, 0x8b, 0x08, 0x71, 0xc0, 0x24, 0x94, 0x9c, 0xb8, 0xd8, 0x83, 0x52,
|
||||
0x0b, 0x4b, 0x53, 0x8b, 0x4b, 0x84, 0x54, 0xb9, 0xf8, 0x8a, 0x53, 0x8b, 0xca, 0x32, 0x93, 0x53,
|
||||
0xe3, 0x73, 0x53, 0x4b, 0x32, 0xf2, 0x53, 0x24, 0x18, 0x15, 0x18, 0x35, 0x38, 0x83, 0x78, 0xa1,
|
||||
0xa2, 0xbe, 0x60, 0x41, 0x21, 0x01, 0x2e, 0xe6, 0xe2, 0xd4, 0x42, 0x09, 0x26, 0x05, 0x46, 0x0d,
|
||||
0xb6, 0x20, 0x10, 0x53, 0x29, 0x92, 0x8b, 0x23, 0x28, 0xb5, 0xb8, 0x20, 0x3f, 0xaf, 0x38, 0x95,
|
||||
0x6c, 0x43, 0x84, 0x44, 0xb8, 0x58, 0x53, 0x8b, 0x8a, 0xf2, 0x8b, 0x24, 0x98, 0xc1, 0xea, 0x21,
|
||||
0x9c, 0x24, 0x36, 0xb0, 0x43, 0x8d, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0xe4, 0x73, 0x3a, 0x4b,
|
||||
0xd0, 0x00, 0x00, 0x00,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Code generated by protoc-gen-micro. DO NOT EDIT.
|
||||
// source: codec/protorpc/envelope.proto
|
||||
|
||||
package protorpc
|
||||
|
||||
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
|
||||
@@ -0,0 +1,14 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package protorpc;
|
||||
|
||||
message Request {
|
||||
string service_method = 1;
|
||||
fixed64 seq = 2;
|
||||
}
|
||||
|
||||
message Response {
|
||||
string service_method = 1;
|
||||
fixed64 seq = 2;
|
||||
string error = 3;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package protorpc
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
)
|
||||
|
||||
// WriteNetString writes data to a big-endian netstring on a Writer.
|
||||
// Size is always a 32-bit unsigned int.
|
||||
func WriteNetString(w io.Writer, data []byte) (written int, err error) {
|
||||
size := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(size, uint32(len(data)))
|
||||
if written, err = w.Write(size); err != nil {
|
||||
return
|
||||
}
|
||||
return w.Write(data)
|
||||
}
|
||||
|
||||
// ReadNetString reads data from a big-endian netstring.
|
||||
func ReadNetString(r io.Reader) (data []byte, err error) {
|
||||
sizeBuf := make([]byte, 4)
|
||||
_, err = r.Read(sizeBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
size := binary.BigEndian.Uint32(sizeBuf)
|
||||
if size == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
data = make([]byte, size)
|
||||
_, err = r.Read(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// Protorpc provides a net/rpc proto-rpc codec. See envelope.proto for the format.
|
||||
package protorpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
type flusher interface {
|
||||
Flush() error
|
||||
}
|
||||
|
||||
type protoCodec struct {
|
||||
rwc io.ReadWriteCloser
|
||||
buf *bytes.Buffer
|
||||
mt codec.MessageType
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func (c *protoCodec) Close() error {
|
||||
c.buf.Reset()
|
||||
return c.rwc.Close()
|
||||
}
|
||||
|
||||
func (c *protoCodec) String() string {
|
||||
return "proto-rpc"
|
||||
}
|
||||
|
||||
func id(id string) uint64 {
|
||||
p, err := strconv.ParseInt(id, 10, 64)
|
||||
if err != nil {
|
||||
p = 0
|
||||
}
|
||||
i := uint64(p)
|
||||
return i
|
||||
}
|
||||
|
||||
func (c *protoCodec) Write(m *codec.Message, b interface{}) error {
|
||||
switch m.Type {
|
||||
case codec.Request:
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
// This is protobuf, of course we copy it.
|
||||
pbr := &Request{ServiceMethod: m.Method, Seq: id(m.Id)}
|
||||
data, err := proto.Marshal(pbr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = WriteNetString(c.rwc, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// dont trust or incoming message
|
||||
m, ok := b.(proto.Message)
|
||||
if !ok {
|
||||
return codec.ErrInvalidMessage
|
||||
}
|
||||
data, err = proto.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = WriteNetString(c.rwc, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if flusher, ok := c.rwc.(flusher); ok {
|
||||
if err = flusher.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case codec.Response, codec.Error:
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
rtmp := &Response{ServiceMethod: m.Method, Seq: id(m.Id), Error: m.Error}
|
||||
data, err := proto.Marshal(rtmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = WriteNetString(c.rwc, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pb, ok := b.(proto.Message); ok {
|
||||
data, err = proto.Marshal(pb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
data = nil
|
||||
}
|
||||
_, err = WriteNetString(c.rwc, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if flusher, ok := c.rwc.(flusher); ok {
|
||||
if err = flusher.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case codec.Event:
|
||||
m, ok := b.(proto.Message)
|
||||
if !ok {
|
||||
return codec.ErrInvalidMessage
|
||||
}
|
||||
data, err := proto.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.rwc.Write(data)
|
||||
default:
|
||||
return fmt.Errorf("Unrecognized message type: %v", m.Type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *protoCodec) ReadHeader(m *codec.Message, mt codec.MessageType) error {
|
||||
c.buf.Reset()
|
||||
c.mt = mt
|
||||
|
||||
switch mt {
|
||||
case codec.Request:
|
||||
data, err := ReadNetString(c.rwc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rtmp := new(Request)
|
||||
err = proto.Unmarshal(data, rtmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Method = rtmp.GetServiceMethod()
|
||||
m.Id = fmt.Sprintf("%d", rtmp.GetSeq())
|
||||
case codec.Response:
|
||||
data, err := ReadNetString(c.rwc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rtmp := new(Response)
|
||||
err = proto.Unmarshal(data, rtmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Method = rtmp.GetServiceMethod()
|
||||
m.Id = fmt.Sprintf("%d", rtmp.GetSeq())
|
||||
m.Error = rtmp.GetError()
|
||||
case codec.Event:
|
||||
_, err := io.Copy(c.buf, c.rwc)
|
||||
return err
|
||||
default:
|
||||
return fmt.Errorf("Unrecognized message type: %v", mt)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *protoCodec) ReadBody(b interface{}) error {
|
||||
var data []byte
|
||||
switch c.mt {
|
||||
case codec.Request, codec.Response:
|
||||
var err error
|
||||
data, err = ReadNetString(c.rwc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case codec.Event:
|
||||
data = c.buf.Bytes()
|
||||
default:
|
||||
return fmt.Errorf("Unrecognized message type: %v", c.mt)
|
||||
}
|
||||
if b != nil {
|
||||
return proto.Unmarshal(data, b.(proto.Message))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewCodec(rwc io.ReadWriteCloser) codec.Codec {
|
||||
return &protoCodec{
|
||||
buf: bytes.NewBuffer(nil),
|
||||
rwc: rwc,
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# Config [](https://godoc.org/github.com/micro/go-micro/config)
|
||||
|
||||
Config is a pluggable dynamic config package
|
||||
|
||||
Most config in applications are statically configured or include complex logic to load from multiple sources.
|
||||
Go Config makes this easy, pluggable and mergeable. You'll never have to deal with config in the same way again.
|
||||
|
||||
## Features
|
||||
|
||||
- **Dynamic Loading** - Load configuration from multiple source as and when needed. Go Config manages watching config sources
|
||||
in the background and automatically merges and updates an in memory view.
|
||||
|
||||
- **Pluggable Sources** - Choose from any number of sources to load and merge config. The backend source is abstracted away into
|
||||
a standard format consumed internally and decoded via encoders. Sources can be env vars, flags, file, etcd, k8s configmap, etc.
|
||||
|
||||
- **Mergeable Config** - If you specify multiple sources of config, regardless of format, they will be merged and presented in
|
||||
a single view. This massively simplifies priority order loading and changes based on environment.
|
||||
|
||||
- **Observe Changes** - Optionally watch the config for changes to specific values. Hot reload your app using Go Config's watcher.
|
||||
You don't have to handle ad-hoc hup reloading or whatever else, just keep reading the config and watch for changes if you need
|
||||
to be notified.
|
||||
|
||||
- **Sane Defaults** - In case config loads badly or is completely wiped away for some unknown reason, you can specify fallback
|
||||
values when accessing any config values directly. This ensures you'll always be reading some sane default in the event of a problem.
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
// Package config is an interface for dynamic configuration.
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/config/loader"
|
||||
"go-micro.dev/v4/config/reader"
|
||||
"go-micro.dev/v4/config/source"
|
||||
"go-micro.dev/v4/config/source/file"
|
||||
)
|
||||
|
||||
// Config is an interface abstraction for dynamic configuration.
|
||||
type Config interface {
|
||||
// provide the reader.Values interface
|
||||
reader.Values
|
||||
// Init the config
|
||||
Init(opts ...Option) error
|
||||
// Options in the config
|
||||
Options() Options
|
||||
// Stop the config loader/watcher
|
||||
Close() error
|
||||
// Load config sources
|
||||
Load(source ...source.Source) error
|
||||
// Force a source changeset sync
|
||||
Sync() error
|
||||
// Watch a value for changes
|
||||
Watch(path ...string) (Watcher, error)
|
||||
}
|
||||
|
||||
// Watcher is the config watcher.
|
||||
type Watcher interface {
|
||||
Next() (reader.Value, error)
|
||||
Stop() error
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Loader loader.Loader
|
||||
Reader reader.Reader
|
||||
|
||||
// for alternative data
|
||||
Context context.Context
|
||||
|
||||
Source []source.Source
|
||||
|
||||
WithWatcherDisabled bool
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
var (
|
||||
// Default Config Manager.
|
||||
DefaultConfig, _ = NewConfig()
|
||||
)
|
||||
|
||||
// NewConfig returns new config.
|
||||
func NewConfig(opts ...Option) (Config, error) {
|
||||
return newConfig(opts...)
|
||||
}
|
||||
|
||||
// Return config as raw json.
|
||||
func Bytes() []byte {
|
||||
return DefaultConfig.Bytes()
|
||||
}
|
||||
|
||||
// Return config as a map.
|
||||
func Map() map[string]interface{} {
|
||||
return DefaultConfig.Map()
|
||||
}
|
||||
|
||||
// Scan values to a go type.
|
||||
func Scan(v interface{}) error {
|
||||
return DefaultConfig.Scan(v)
|
||||
}
|
||||
|
||||
// Force a source changeset sync.
|
||||
func Sync() error {
|
||||
return DefaultConfig.Sync()
|
||||
}
|
||||
|
||||
// Get a value from the config.
|
||||
func Get(path ...string) reader.Value {
|
||||
return DefaultConfig.Get(path...)
|
||||
}
|
||||
|
||||
// Load config sources.
|
||||
func Load(source ...source.Source) error {
|
||||
return DefaultConfig.Load(source...)
|
||||
}
|
||||
|
||||
// Watch a value for changes.
|
||||
func Watch(path ...string) (Watcher, error) {
|
||||
return DefaultConfig.Watch(path...)
|
||||
}
|
||||
|
||||
// LoadFile is short hand for creating a file source and loading it.
|
||||
func LoadFile(path string) error {
|
||||
return Load(file.NewSource(
|
||||
file.WithPath(path),
|
||||
))
|
||||
}
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go-micro.dev/v4/config/loader"
|
||||
"go-micro.dev/v4/config/loader/memory"
|
||||
"go-micro.dev/v4/config/reader"
|
||||
"go-micro.dev/v4/config/reader/json"
|
||||
"go-micro.dev/v4/config/source"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
// the current values
|
||||
vals reader.Values
|
||||
exit chan bool
|
||||
// the current snapshot
|
||||
snap *loader.Snapshot
|
||||
opts Options
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
type watcher struct {
|
||||
lw loader.Watcher
|
||||
rd reader.Reader
|
||||
value reader.Value
|
||||
path []string
|
||||
}
|
||||
|
||||
func newConfig(opts ...Option) (Config, error) {
|
||||
var c config
|
||||
|
||||
err := c.Init(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !c.opts.WithWatcherDisabled {
|
||||
go c.run()
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (c *config) Init(opts ...Option) error {
|
||||
c.opts = Options{
|
||||
Reader: json.NewReader(),
|
||||
}
|
||||
c.exit = make(chan bool)
|
||||
for _, o := range opts {
|
||||
o(&c.opts)
|
||||
}
|
||||
|
||||
// default loader uses the configured reader
|
||||
if c.opts.Loader == nil {
|
||||
loaderOpts := []loader.Option{memory.WithReader(c.opts.Reader)}
|
||||
if c.opts.WithWatcherDisabled {
|
||||
loaderOpts = append(loaderOpts, memory.WithWatcherDisabled())
|
||||
}
|
||||
|
||||
c.opts.Loader = memory.NewLoader(loaderOpts...)
|
||||
}
|
||||
|
||||
err := c.opts.Loader.Load(c.opts.Source...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.snap, err = c.opts.Loader.Snapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.vals, err = c.opts.Reader.Values(c.snap.ChangeSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *config) Options() Options {
|
||||
return c.opts
|
||||
}
|
||||
|
||||
func (c *config) run() {
|
||||
watch := func(w loader.Watcher) error {
|
||||
for {
|
||||
// get changeset
|
||||
snap, err := w.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
|
||||
if c.snap.Version >= snap.Version {
|
||||
c.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
// save
|
||||
c.snap = snap
|
||||
|
||||
// set values
|
||||
c.vals, _ = c.opts.Reader.Values(snap.ChangeSet)
|
||||
|
||||
c.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
w, err := c.opts.Loader.Watch()
|
||||
if err != nil {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
done := make(chan bool)
|
||||
|
||||
// the stop watch func
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
case <-c.exit:
|
||||
}
|
||||
err := w.Stop()
|
||||
fmt.Println(err.Error())
|
||||
}()
|
||||
|
||||
// block watch
|
||||
if err := watch(w); err != nil {
|
||||
// do something better
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
// close done chan
|
||||
close(done)
|
||||
|
||||
// if the config is closed exit
|
||||
select {
|
||||
case <-c.exit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *config) Map() map[string]interface{} {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
return c.vals.Map()
|
||||
}
|
||||
|
||||
func (c *config) Scan(v interface{}) error {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
return c.vals.Scan(v)
|
||||
}
|
||||
|
||||
// sync loads all the sources, calls the parser and updates the config.
|
||||
func (c *config) Sync() error {
|
||||
if err := c.opts.Loader.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
snap, err := c.opts.Loader.Snapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
c.snap = snap
|
||||
vals, err := c.opts.Reader.Values(snap.ChangeSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.vals = vals
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *config) Close() error {
|
||||
select {
|
||||
case <-c.exit:
|
||||
return nil
|
||||
default:
|
||||
close(c.exit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *config) Get(path ...string) reader.Value {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
// did sync actually work?
|
||||
if c.vals != nil {
|
||||
return c.vals.Get(path...)
|
||||
}
|
||||
|
||||
// no value
|
||||
return newValue()
|
||||
}
|
||||
|
||||
func (c *config) Set(val interface{}, path ...string) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
if c.vals != nil {
|
||||
c.vals.Set(val, path...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *config) Del(path ...string) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
if c.vals != nil {
|
||||
c.vals.Del(path...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *config) Bytes() []byte {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
if c.vals == nil {
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
return c.vals.Bytes()
|
||||
}
|
||||
|
||||
func (c *config) Load(sources ...source.Source) error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
if err := c.opts.Loader.Load(sources...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
snap, err := c.opts.Loader.Snapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.snap = snap
|
||||
vals, err := c.opts.Reader.Values(snap.ChangeSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.vals = vals
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *config) Watch(path ...string) (Watcher, error) {
|
||||
value := c.Get(path...)
|
||||
|
||||
w, err := c.opts.Loader.Watch(path...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &watcher{
|
||||
lw: w,
|
||||
rd: c.opts.Reader,
|
||||
path: path,
|
||||
value: value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *config) String() string {
|
||||
return "config"
|
||||
}
|
||||
|
||||
func (w *watcher) Next() (reader.Value, error) {
|
||||
for {
|
||||
s, err := w.lw.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// only process changes
|
||||
if bytes.Equal(w.value.Bytes(), s.ChangeSet.Data) {
|
||||
continue
|
||||
}
|
||||
|
||||
v, err := w.rd.Values(s.ChangeSet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w.value = v.Get()
|
||||
return w.value, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) Stop() error {
|
||||
return w.lw.Stop()
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Package encoder handles source encoding formats
|
||||
package encoder
|
||||
|
||||
type Encoder interface {
|
||||
Encode(interface{}) ([]byte, error)
|
||||
Decode([]byte, interface{}) error
|
||||
String() string
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"go-micro.dev/v4/config/encoder"
|
||||
)
|
||||
|
||||
type jsonEncoder struct{}
|
||||
|
||||
func (j jsonEncoder) Encode(v interface{}) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (j jsonEncoder) Decode(d []byte, v interface{}) error {
|
||||
return json.Unmarshal(d, v)
|
||||
}
|
||||
|
||||
func (j jsonEncoder) String() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
func NewEncoder() encoder.Encoder {
|
||||
return jsonEncoder{}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Package loader manages loading from multiple sources
|
||||
package loader
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/config/reader"
|
||||
"go-micro.dev/v4/config/source"
|
||||
)
|
||||
|
||||
// Loader manages loading sources.
|
||||
type Loader interface {
|
||||
// Stop the loader
|
||||
Close() error
|
||||
// Load the sources
|
||||
Load(...source.Source) error
|
||||
// A Snapshot of loaded config
|
||||
Snapshot() (*Snapshot, error)
|
||||
// Force sync of sources
|
||||
Sync() error
|
||||
// Watch for changes
|
||||
Watch(...string) (Watcher, error)
|
||||
// Name of loader
|
||||
String() string
|
||||
}
|
||||
|
||||
// Watcher lets you watch sources and returns a merged ChangeSet.
|
||||
type Watcher interface {
|
||||
// First call to next may return the current Snapshot
|
||||
// If you are watching a path then only the data from
|
||||
// that path is returned.
|
||||
Next() (*Snapshot, error)
|
||||
// Stop watching for changes
|
||||
Stop() error
|
||||
}
|
||||
|
||||
// Snapshot is a merged ChangeSet.
|
||||
type Snapshot struct {
|
||||
// The merged ChangeSet
|
||||
ChangeSet *source.ChangeSet
|
||||
// Deterministic and comparable version of the snapshot
|
||||
Version string
|
||||
}
|
||||
|
||||
// Options contains all options for a config loader.
|
||||
type Options struct {
|
||||
Reader reader.Reader
|
||||
|
||||
// for alternative data
|
||||
Context context.Context
|
||||
|
||||
Source []source.Source
|
||||
|
||||
WithWatcherDisabled bool
|
||||
}
|
||||
|
||||
// Option is a helper for a single option.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Copy snapshot.
|
||||
func Copy(s *Snapshot) *Snapshot {
|
||||
cs := *(s.ChangeSet)
|
||||
|
||||
return &Snapshot{
|
||||
ChangeSet: &cs,
|
||||
Version: s.Version,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"container/list"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v4/config/loader"
|
||||
"go-micro.dev/v4/config/reader"
|
||||
"go-micro.dev/v4/config/reader/json"
|
||||
"go-micro.dev/v4/config/source"
|
||||
)
|
||||
|
||||
type memory struct {
|
||||
// the current values
|
||||
vals reader.Values
|
||||
exit chan bool
|
||||
// the current snapshot
|
||||
snap *loader.Snapshot
|
||||
|
||||
watchers *list.List
|
||||
opts loader.Options
|
||||
|
||||
// all the sets
|
||||
sets []*source.ChangeSet
|
||||
// all the sources
|
||||
sources []source.Source
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
type updateValue struct {
|
||||
value reader.Value
|
||||
version string
|
||||
}
|
||||
|
||||
type watcher struct {
|
||||
value reader.Value
|
||||
reader reader.Reader
|
||||
version atomic.Value
|
||||
exit chan bool
|
||||
updates chan updateValue
|
||||
path []string
|
||||
}
|
||||
|
||||
func (w *watcher) getVersion() string {
|
||||
return w.version.Load().(string)
|
||||
}
|
||||
|
||||
func (m *memory) watch(idx int, s source.Source) {
|
||||
// watches a source for changes
|
||||
watch := func(idx int, s source.Watcher) error {
|
||||
for {
|
||||
// get changeset
|
||||
cs, err := s.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
|
||||
// save
|
||||
m.sets[idx] = cs
|
||||
|
||||
// merge sets
|
||||
set, err := m.opts.Reader.Merge(m.sets...)
|
||||
if err != nil {
|
||||
m.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// set values
|
||||
m.vals, _ = m.opts.Reader.Values(set)
|
||||
m.snap = &loader.Snapshot{
|
||||
ChangeSet: set,
|
||||
Version: genVer(),
|
||||
}
|
||||
m.Unlock()
|
||||
|
||||
// send watch updates
|
||||
m.update()
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
// watch the source
|
||||
w, err := s.Watch()
|
||||
if err != nil {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
done := make(chan bool)
|
||||
|
||||
// the stop watch func
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
case <-m.exit:
|
||||
}
|
||||
w.Stop()
|
||||
}()
|
||||
|
||||
// block watch
|
||||
if err := watch(idx, w); err != nil {
|
||||
// do something better
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
// close done chan
|
||||
close(done)
|
||||
|
||||
// if the config is closed exit
|
||||
select {
|
||||
case <-m.exit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *memory) loaded() bool {
|
||||
var loaded bool
|
||||
m.RLock()
|
||||
if m.vals != nil {
|
||||
loaded = true
|
||||
}
|
||||
m.RUnlock()
|
||||
return loaded
|
||||
}
|
||||
|
||||
// reload reads the sets and creates new values.
|
||||
func (m *memory) reload() error {
|
||||
m.Lock()
|
||||
|
||||
// merge sets
|
||||
set, err := m.opts.Reader.Merge(m.sets...)
|
||||
if err != nil {
|
||||
m.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// set values
|
||||
if vals, err := m.opts.Reader.Values(set); err != nil {
|
||||
m.vals = vals
|
||||
}
|
||||
m.snap = &loader.Snapshot{
|
||||
ChangeSet: set,
|
||||
Version: genVer(),
|
||||
}
|
||||
|
||||
m.Unlock()
|
||||
|
||||
// update watchers
|
||||
m.update()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memory) update() {
|
||||
m.RLock()
|
||||
|
||||
watchers := make([]*watcher, 0, m.watchers.Len())
|
||||
|
||||
for e := m.watchers.Front(); e != nil; e = e.Next() {
|
||||
watchers = append(watchers, e.Value.(*watcher))
|
||||
}
|
||||
|
||||
vals := m.vals
|
||||
snap := m.snap
|
||||
m.RUnlock()
|
||||
|
||||
for _, w := range watchers {
|
||||
if w.getVersion() >= snap.Version {
|
||||
continue
|
||||
}
|
||||
|
||||
uv := updateValue{
|
||||
version: m.snap.Version,
|
||||
value: vals.Get(w.path...),
|
||||
}
|
||||
|
||||
select {
|
||||
case w.updates <- uv:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot returns a snapshot of the current loaded config.
|
||||
func (m *memory) Snapshot() (*loader.Snapshot, error) {
|
||||
if m.loaded() {
|
||||
m.RLock()
|
||||
snap := loader.Copy(m.snap)
|
||||
m.RUnlock()
|
||||
return snap, nil
|
||||
}
|
||||
|
||||
// not loaded, sync
|
||||
if err := m.Sync(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// make copy
|
||||
m.RLock()
|
||||
snap := loader.Copy(m.snap)
|
||||
m.RUnlock()
|
||||
|
||||
return snap, nil
|
||||
}
|
||||
|
||||
// Sync loads all the sources, calls the parser and updates the config.
|
||||
func (m *memory) Sync() error {
|
||||
//nolint:prealloc
|
||||
var sets []*source.ChangeSet
|
||||
|
||||
m.Lock()
|
||||
|
||||
// read the source
|
||||
var gerr []string
|
||||
|
||||
for _, source := range m.sources {
|
||||
ch, err := source.Read()
|
||||
if err != nil {
|
||||
gerr = append(gerr, err.Error())
|
||||
continue
|
||||
}
|
||||
sets = append(sets, ch)
|
||||
}
|
||||
|
||||
// merge sets
|
||||
set, err := m.opts.Reader.Merge(sets...)
|
||||
if err != nil {
|
||||
m.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// set values
|
||||
vals, err := m.opts.Reader.Values(set)
|
||||
if err != nil {
|
||||
m.Unlock()
|
||||
return err
|
||||
}
|
||||
m.vals = vals
|
||||
m.snap = &loader.Snapshot{
|
||||
ChangeSet: set,
|
||||
Version: genVer(),
|
||||
}
|
||||
|
||||
m.Unlock()
|
||||
|
||||
// update watchers
|
||||
m.update()
|
||||
|
||||
if len(gerr) > 0 {
|
||||
return fmt.Errorf("source loading errors: %s", strings.Join(gerr, "\n"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memory) Close() error {
|
||||
select {
|
||||
case <-m.exit:
|
||||
return nil
|
||||
default:
|
||||
close(m.exit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memory) Get(path ...string) (reader.Value, error) {
|
||||
if !m.loaded() {
|
||||
if err := m.Sync(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
// did sync actually work?
|
||||
if m.vals != nil {
|
||||
return m.vals.Get(path...), nil
|
||||
}
|
||||
|
||||
// assuming vals is nil
|
||||
// create new vals
|
||||
|
||||
ch := m.snap.ChangeSet
|
||||
|
||||
// we are truly screwed, trying to load in a hacked way
|
||||
v, err := m.opts.Reader.Values(ch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// lets set it just because
|
||||
m.vals = v
|
||||
|
||||
if m.vals != nil {
|
||||
return m.vals.Get(path...), nil
|
||||
}
|
||||
|
||||
// ok we're going hardcore now
|
||||
|
||||
return nil, errors.New("no values")
|
||||
}
|
||||
|
||||
func (m *memory) Load(sources ...source.Source) error {
|
||||
var gerrors []string
|
||||
|
||||
for _, source := range sources {
|
||||
set, err := source.Read()
|
||||
if err != nil {
|
||||
gerrors = append(gerrors,
|
||||
fmt.Sprintf("error loading source %s: %v",
|
||||
source,
|
||||
err))
|
||||
// continue processing
|
||||
continue
|
||||
}
|
||||
m.Lock()
|
||||
m.sources = append(m.sources, source)
|
||||
m.sets = append(m.sets, set)
|
||||
idx := len(m.sets) - 1
|
||||
m.Unlock()
|
||||
if !m.opts.WithWatcherDisabled {
|
||||
go m.watch(idx, source)
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.reload(); err != nil {
|
||||
gerrors = append(gerrors, err.Error())
|
||||
}
|
||||
|
||||
// Return errors
|
||||
if len(gerrors) != 0 {
|
||||
return errors.New(strings.Join(gerrors, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memory) Watch(path ...string) (loader.Watcher, error) {
|
||||
if m.opts.WithWatcherDisabled {
|
||||
return nil, errors.New("watcher is disabled")
|
||||
}
|
||||
|
||||
value, err := m.Get(path...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
|
||||
w := &watcher{
|
||||
exit: make(chan bool),
|
||||
path: path,
|
||||
value: value,
|
||||
reader: m.opts.Reader,
|
||||
updates: make(chan updateValue, 1),
|
||||
}
|
||||
w.version.Store(m.snap.Version)
|
||||
|
||||
e := m.watchers.PushBack(w)
|
||||
|
||||
m.Unlock()
|
||||
|
||||
go func() {
|
||||
<-w.exit
|
||||
m.Lock()
|
||||
m.watchers.Remove(e)
|
||||
m.Unlock()
|
||||
}()
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (m *memory) String() string {
|
||||
return "memory"
|
||||
}
|
||||
|
||||
func (w *watcher) Next() (*loader.Snapshot, error) {
|
||||
update := func(v reader.Value) *loader.Snapshot {
|
||||
w.value = v
|
||||
|
||||
cs := &source.ChangeSet{
|
||||
Data: v.Bytes(),
|
||||
Format: w.reader.String(),
|
||||
Source: "memory",
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
cs.Checksum = cs.Sum()
|
||||
|
||||
return &loader.Snapshot{
|
||||
ChangeSet: cs,
|
||||
Version: w.getVersion(),
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.exit:
|
||||
return nil, errors.New("watcher stopped")
|
||||
|
||||
case uv := <-w.updates:
|
||||
if uv.version <= w.getVersion() {
|
||||
continue
|
||||
}
|
||||
|
||||
v := uv.value
|
||||
|
||||
w.version.Store(uv.version)
|
||||
|
||||
if bytes.Equal(w.value.Bytes(), v.Bytes()) {
|
||||
continue
|
||||
}
|
||||
|
||||
return update(v), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) Stop() error {
|
||||
select {
|
||||
case <-w.exit:
|
||||
default:
|
||||
close(w.exit)
|
||||
close(w.updates)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func genVer() string {
|
||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func NewLoader(opts ...loader.Option) loader.Loader {
|
||||
options := loader.Options{
|
||||
Reader: json.NewReader(),
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
m := &memory{
|
||||
exit: make(chan bool),
|
||||
opts: options,
|
||||
watchers: list.New(),
|
||||
sources: options.Source,
|
||||
}
|
||||
|
||||
m.sets = make([]*source.ChangeSet, len(options.Source))
|
||||
|
||||
for i, s := range options.Source {
|
||||
m.sets[i] = &source.ChangeSet{Source: s.String()}
|
||||
if !options.WithWatcherDisabled {
|
||||
go m.watch(i, s)
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"go-micro.dev/v4/config/loader"
|
||||
"go-micro.dev/v4/config/reader"
|
||||
"go-micro.dev/v4/config/source"
|
||||
)
|
||||
|
||||
// WithSource appends a source to list of sources.
|
||||
func WithSource(s source.Source) loader.Option {
|
||||
return func(o *loader.Options) {
|
||||
o.Source = append(o.Source, s)
|
||||
}
|
||||
}
|
||||
|
||||
// WithReader sets the config reader.
|
||||
func WithReader(r reader.Reader) loader.Option {
|
||||
return func(o *loader.Options) {
|
||||
o.Reader = r
|
||||
}
|
||||
}
|
||||
|
||||
func WithWatcherDisabled() loader.Option {
|
||||
return func(o *loader.Options) {
|
||||
o.WithWatcherDisabled = true
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"go-micro.dev/v4/config/loader"
|
||||
"go-micro.dev/v4/config/reader"
|
||||
"go-micro.dev/v4/config/source"
|
||||
)
|
||||
|
||||
// WithLoader sets the loader for manager config.
|
||||
func WithLoader(l loader.Loader) Option {
|
||||
return func(o *Options) {
|
||||
o.Loader = l
|
||||
}
|
||||
}
|
||||
|
||||
// WithSource appends a source to list of sources.
|
||||
func WithSource(s source.Source) Option {
|
||||
return func(o *Options) {
|
||||
o.Source = append(o.Source, s)
|
||||
}
|
||||
}
|
||||
|
||||
// WithReader sets the config reader.
|
||||
func WithReader(r reader.Reader) Option {
|
||||
return func(o *Options) {
|
||||
o.Reader = r
|
||||
}
|
||||
}
|
||||
|
||||
func WithWatcherDisabled() Option {
|
||||
return func(o *Options) {
|
||||
o.WithWatcherDisabled = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"go-micro.dev/v4/config/encoder"
|
||||
"go-micro.dev/v4/config/encoder/json"
|
||||
"go-micro.dev/v4/config/reader"
|
||||
"go-micro.dev/v4/config/source"
|
||||
)
|
||||
|
||||
type jsonReader struct {
|
||||
opts reader.Options
|
||||
json encoder.Encoder
|
||||
}
|
||||
|
||||
func (j *jsonReader) Merge(changes ...*source.ChangeSet) (*source.ChangeSet, error) {
|
||||
var merged map[string]interface{}
|
||||
|
||||
for _, m := range changes {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(m.Data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
codec, ok := j.opts.Encoding[m.Format]
|
||||
if !ok {
|
||||
// fallback
|
||||
codec = j.json
|
||||
}
|
||||
|
||||
var data map[string]interface{}
|
||||
if err := codec.Decode(m.Data, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mergo.Map(&merged, data, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
b, err := j.json.Encode(merged)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cs := &source.ChangeSet{
|
||||
Timestamp: time.Now(),
|
||||
Data: b,
|
||||
Source: "json",
|
||||
Format: j.json.String(),
|
||||
}
|
||||
cs.Checksum = cs.Sum()
|
||||
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
func (j *jsonReader) Values(ch *source.ChangeSet) (reader.Values, error) {
|
||||
if ch == nil {
|
||||
return nil, errors.New("changeset is nil")
|
||||
}
|
||||
if ch.Format != "json" {
|
||||
return nil, errors.New("unsupported format")
|
||||
}
|
||||
return newValues(ch)
|
||||
}
|
||||
|
||||
func (j *jsonReader) String() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
// NewReader creates a json reader.
|
||||
func NewReader(opts ...reader.Option) reader.Reader {
|
||||
options := reader.NewOptions(opts...)
|
||||
return &jsonReader{
|
||||
json: json.NewEncoder(),
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
simple "github.com/bitly/go-simplejson"
|
||||
"go-micro.dev/v4/config/reader"
|
||||
"go-micro.dev/v4/config/source"
|
||||
)
|
||||
|
||||
type jsonValues struct {
|
||||
ch *source.ChangeSet
|
||||
sj *simple.Json
|
||||
}
|
||||
|
||||
type jsonValue struct {
|
||||
*simple.Json
|
||||
}
|
||||
|
||||
func newValues(ch *source.ChangeSet) (reader.Values, error) {
|
||||
sj := simple.New()
|
||||
data, _ := reader.ReplaceEnvVars(ch.Data)
|
||||
if err := sj.UnmarshalJSON(data); err != nil {
|
||||
sj.SetPath(nil, string(ch.Data))
|
||||
}
|
||||
return &jsonValues{ch, sj}, nil
|
||||
}
|
||||
|
||||
func (j *jsonValues) Get(path ...string) reader.Value {
|
||||
return &jsonValue{j.sj.GetPath(path...)}
|
||||
}
|
||||
|
||||
func (j *jsonValues) Del(path ...string) {
|
||||
// delete the tree?
|
||||
if len(path) == 0 {
|
||||
j.sj = simple.New()
|
||||
return
|
||||
}
|
||||
|
||||
if len(path) == 1 {
|
||||
j.sj.Del(path[0])
|
||||
return
|
||||
}
|
||||
|
||||
vals := j.sj.GetPath(path[:len(path)-1]...)
|
||||
vals.Del(path[len(path)-1])
|
||||
j.sj.SetPath(path[:len(path)-1], vals.Interface())
|
||||
return
|
||||
}
|
||||
|
||||
func (j *jsonValues) Set(val interface{}, path ...string) {
|
||||
j.sj.SetPath(path, val)
|
||||
}
|
||||
|
||||
func (j *jsonValues) Bytes() []byte {
|
||||
b, _ := j.sj.MarshalJSON()
|
||||
return b
|
||||
}
|
||||
|
||||
func (j *jsonValues) Map() map[string]interface{} {
|
||||
m, _ := j.sj.Map()
|
||||
return m
|
||||
}
|
||||
|
||||
func (j *jsonValues) Scan(v interface{}) error {
|
||||
b, err := j.sj.MarshalJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
func (j *jsonValues) String() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
func (j *jsonValue) Bool(def bool) bool {
|
||||
b, err := j.Json.Bool()
|
||||
if err == nil {
|
||||
return b
|
||||
}
|
||||
|
||||
str, ok := j.Interface().(string)
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
|
||||
b, err = strconv.ParseBool(str)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (j *jsonValue) Int(def int) int {
|
||||
i, err := j.Json.Int()
|
||||
if err == nil {
|
||||
return i
|
||||
}
|
||||
|
||||
str, ok := j.Interface().(string)
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
|
||||
i, err = strconv.Atoi(str)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
func (j *jsonValue) String(def string) string {
|
||||
return j.Json.MustString(def)
|
||||
}
|
||||
|
||||
func (j *jsonValue) Float64(def float64) float64 {
|
||||
f, err := j.Json.Float64()
|
||||
if err == nil {
|
||||
return f
|
||||
}
|
||||
|
||||
str, ok := j.Interface().(string)
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
|
||||
f, err = strconv.ParseFloat(str, 64)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (j *jsonValue) Duration(def time.Duration) time.Duration {
|
||||
v, err := j.Json.String()
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
value, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func (j *jsonValue) StringSlice(def []string) []string {
|
||||
v, err := j.Json.String()
|
||||
if err == nil {
|
||||
sl := strings.Split(v, ",")
|
||||
if len(sl) > 0 {
|
||||
return sl
|
||||
}
|
||||
}
|
||||
return j.Json.MustStringArray(def)
|
||||
}
|
||||
|
||||
func (j *jsonValue) StringMap(def map[string]string) map[string]string {
|
||||
m, err := j.Json.Map()
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
res := map[string]string{}
|
||||
|
||||
for k, v := range m {
|
||||
res[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (j *jsonValue) Scan(v interface{}) error {
|
||||
b, err := j.Json.MarshalJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
func (j *jsonValue) Bytes() []byte {
|
||||
b, err := j.Json.Bytes()
|
||||
if err != nil {
|
||||
// try return marshaled
|
||||
b, err = j.Json.MarshalJSON()
|
||||
if err != nil {
|
||||
return []byte{}
|
||||
}
|
||||
return b
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package reader
|
||||
|
||||
import (
|
||||
"go-micro.dev/v4/config/encoder"
|
||||
"go-micro.dev/v4/config/encoder/json"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Encoding map[string]encoder.Encoder
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{
|
||||
Encoding: map[string]encoder.Encoder{
|
||||
"json": json.NewEncoder(),
|
||||
},
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func WithEncoder(e encoder.Encoder) Option {
|
||||
return func(o *Options) {
|
||||
if o.Encoding == nil {
|
||||
o.Encoding = make(map[string]encoder.Encoder)
|
||||
}
|
||||
o.Encoding[e.String()] = e
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package reader
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func ReplaceEnvVars(raw []byte) ([]byte, error) {
|
||||
re := regexp.MustCompile(`\$\{([A-Za-z0-9_]+)\}`)
|
||||
if re.Match(raw) {
|
||||
dataS := string(raw)
|
||||
res := re.ReplaceAllStringFunc(dataS, replaceEnvVars)
|
||||
return []byte(res), nil
|
||||
} else {
|
||||
return raw, nil
|
||||
}
|
||||
}
|
||||
|
||||
func replaceEnvVars(element string) string {
|
||||
v := element[2 : len(element)-1]
|
||||
el := os.Getenv(v)
|
||||
return el
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Package reader parses change sets and provides config values
|
||||
package reader
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v4/config/source"
|
||||
)
|
||||
|
||||
// Reader is an interface for merging changesets.
|
||||
type Reader interface {
|
||||
Merge(...*source.ChangeSet) (*source.ChangeSet, error)
|
||||
Values(*source.ChangeSet) (Values, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
// Values is returned by the reader.
|
||||
type Values interface {
|
||||
Bytes() []byte
|
||||
Get(path ...string) Value
|
||||
Set(val interface{}, path ...string)
|
||||
Del(path ...string)
|
||||
Map() map[string]interface{}
|
||||
Scan(v interface{}) error
|
||||
}
|
||||
|
||||
// Value represents a value of any type.
|
||||
type Value interface {
|
||||
Bool(def bool) bool
|
||||
Int(def int) int
|
||||
String(def string) string
|
||||
Float64(def float64) float64
|
||||
Duration(def time.Duration) time.Duration
|
||||
StringSlice(def []string) []string
|
||||
StringMap(def map[string]string) map[string]string
|
||||
Scan(val interface{}) error
|
||||
Bytes() []byte
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Sum returns the md5 checksum of the ChangeSet data.
|
||||
func (c *ChangeSet) Sum() string {
|
||||
h := md5.New()
|
||||
h.Write(c.Data)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
# File Source
|
||||
|
||||
The file source reads config from a file.
|
||||
|
||||
It uses the File extension to determine the Format e.g `config.yaml` has the yaml format.
|
||||
It does not make use of encoders or interpet the file data. If a file extension is not present
|
||||
the source Format will default to the Encoder in options.
|
||||
|
||||
## Example
|
||||
|
||||
A config file format in json
|
||||
|
||||
```json
|
||||
{
|
||||
"hosts": {
|
||||
"database": {
|
||||
"address": "10.0.0.1",
|
||||
"port": 3306
|
||||
},
|
||||
"cache": {
|
||||
"address": "10.0.0.2",
|
||||
"port": 6379
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## New Source
|
||||
|
||||
Specify file source with path to file. Path is optional and will default to `config.json`
|
||||
|
||||
```go
|
||||
fileSource := file.NewSource(
|
||||
file.WithPath("/tmp/config.json"),
|
||||
)
|
||||
```
|
||||
|
||||
## File Format
|
||||
|
||||
To load different file formats e.g yaml, toml, xml simply specify them with their extension
|
||||
|
||||
```go
|
||||
fileSource := file.NewSource(
|
||||
file.WithPath("/tmp/config.yaml"),
|
||||
)
|
||||
```
|
||||
|
||||
If you want to specify a file without extension, ensure you set the encoder to the same format
|
||||
|
||||
```go
|
||||
e := toml.NewEncoder()
|
||||
|
||||
fileSource := file.NewSource(
|
||||
file.WithPath("/tmp/config"),
|
||||
source.WithEncoder(e),
|
||||
)
|
||||
```
|
||||
|
||||
## Load Source
|
||||
|
||||
Load the source into config
|
||||
|
||||
```go
|
||||
// Create new config
|
||||
conf := config.NewConfig()
|
||||
|
||||
// Load file source
|
||||
conf.Load(fileSource)
|
||||
```
|
||||
@@ -0,0 +1,87 @@
|
||||
// Package file is a file source. Expected format is json
|
||||
package file
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"go-micro.dev/v4/config/source"
|
||||
)
|
||||
|
||||
type file struct {
|
||||
opts source.Options
|
||||
fs fs.FS
|
||||
path string
|
||||
}
|
||||
|
||||
var (
|
||||
DefaultPath = "config.json"
|
||||
)
|
||||
|
||||
func (f *file) Read() (*source.ChangeSet, error) {
|
||||
var fh fs.File
|
||||
var err error
|
||||
|
||||
if f.fs != nil {
|
||||
fh, err = f.fs.Open(f.path)
|
||||
} else {
|
||||
fh, err = os.Open(f.path)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
b, err := io.ReadAll(fh)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := fh.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cs := &source.ChangeSet{
|
||||
Format: format(f.path, f.opts.Encoder),
|
||||
Source: f.String(),
|
||||
Timestamp: info.ModTime(),
|
||||
Data: b,
|
||||
}
|
||||
cs.Checksum = cs.Sum()
|
||||
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
func (f *file) String() string {
|
||||
return "file"
|
||||
}
|
||||
|
||||
func (f *file) Watch() (source.Watcher, error) {
|
||||
// do not watch if fs.FS instance is provided
|
||||
if f.fs != nil {
|
||||
return source.NewNoopWatcher()
|
||||
}
|
||||
|
||||
if _, err := os.Stat(f.path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newWatcher(f)
|
||||
}
|
||||
|
||||
func (f *file) Write(cs *source.ChangeSet) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewSource(opts ...source.Option) source.Source {
|
||||
options := source.NewOptions(opts...)
|
||||
|
||||
fs, _ := options.Context.Value(fsKey{}).(fs.FS)
|
||||
|
||||
path := DefaultPath
|
||||
f, ok := options.Context.Value(filePathKey{}).(string)
|
||||
if ok {
|
||||
path = f
|
||||
}
|
||||
return &file{opts: options, fs: fs, path: path}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v4/config/encoder"
|
||||
)
|
||||
|
||||
func format(p string, e encoder.Encoder) string {
|
||||
parts := strings.Split(p, ".")
|
||||
if len(parts) > 1 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
return e.String()
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
|
||||
"go-micro.dev/v4/config/source"
|
||||
)
|
||||
|
||||
type filePathKey struct{}
|
||||
type fsKey struct{}
|
||||
|
||||
// WithPath sets the path to file.
|
||||
func WithPath(p string) source.Option {
|
||||
return func(o *source.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, filePathKey{}, p)
|
||||
}
|
||||
}
|
||||
|
||||
// WithFS sets the underlying filesystem to lookup file from (default os.FS).
|
||||
func WithFS(fs fs.FS) source.Option {
|
||||
return func(o *source.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, fsKey{}, fs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"go-micro.dev/v4/config/source"
|
||||
)
|
||||
|
||||
type watcher struct {
|
||||
f *file
|
||||
|
||||
fw *fsnotify.Watcher
|
||||
}
|
||||
|
||||
func newWatcher(f *file) (source.Watcher, error) {
|
||||
fw, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fw.Add(f.path)
|
||||
|
||||
return &watcher{
|
||||
f: f,
|
||||
fw: fw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *watcher) Next() (*source.ChangeSet, error) {
|
||||
// try get the event
|
||||
select {
|
||||
case event, ok := <-w.fw.Events:
|
||||
// check if channel was closed (i.e. Watcher.Close() was called).
|
||||
if !ok {
|
||||
return nil, source.ErrWatcherStopped
|
||||
}
|
||||
|
||||
if event.Has(fsnotify.Rename) {
|
||||
// check existence of file, and add watch again
|
||||
_, err := os.Stat(event.Name)
|
||||
if err == nil || os.IsExist(err) {
|
||||
w.fw.Add(event.Name)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := w.f.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
case err, ok := <-w.fw.Errors:
|
||||
// check if channel was closed (i.e. Watcher.Close() was called).
|
||||
if !ok {
|
||||
return nil, source.ErrWatcherStopped
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) Stop() error {
|
||||
return w.fw.Close()
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"go-micro.dev/v4/config/source"
|
||||
)
|
||||
|
||||
type watcher struct {
|
||||
f *file
|
||||
|
||||
fw *fsnotify.Watcher
|
||||
}
|
||||
|
||||
func newWatcher(f *file) (source.Watcher, error) {
|
||||
fw, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fw.Add(f.path)
|
||||
|
||||
return &watcher{
|
||||
f: f,
|
||||
fw: fw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *watcher) Next() (*source.ChangeSet, error) {
|
||||
// try get the event
|
||||
select {
|
||||
case event, ok := <-w.fw.Events:
|
||||
// check if channel was closed (i.e. Watcher.Close() was called).
|
||||
if !ok {
|
||||
return nil, source.ErrWatcherStopped
|
||||
}
|
||||
|
||||
if event.Has(fsnotify.Rename) {
|
||||
// check existence of file, and add watch again
|
||||
_, err := os.Stat(event.Name)
|
||||
if err == nil || os.IsExist(err) {
|
||||
w.fw.Add(event.Name)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := w.f.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add path again for the event bug of fsnotify
|
||||
w.fw.Add(w.f.path)
|
||||
|
||||
return c, nil
|
||||
case err, ok := <-w.fw.Errors:
|
||||
// check if channel was closed (i.e. Watcher.Close() was called).
|
||||
if !ok {
|
||||
return nil, source.ErrWatcherStopped
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) Stop() error {
|
||||
return w.fw.Close()
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type noopWatcher struct {
|
||||
exit chan struct{}
|
||||
}
|
||||
|
||||
func (w *noopWatcher) Next() (*ChangeSet, error) {
|
||||
<-w.exit
|
||||
|
||||
return nil, errors.New("noopWatcher stopped")
|
||||
}
|
||||
|
||||
func (w *noopWatcher) Stop() error {
|
||||
close(w.exit)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewNoopWatcher returns a watcher that blocks on Next() until Stop() is called.
|
||||
func NewNoopWatcher() (Watcher, error) {
|
||||
return &noopWatcher{exit: make(chan struct{})}, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/client"
|
||||
"go-micro.dev/v4/config/encoder"
|
||||
"go-micro.dev/v4/config/encoder/json"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
// Encoder
|
||||
Encoder encoder.Encoder
|
||||
|
||||
// for alternative data
|
||||
Context context.Context
|
||||
|
||||
// Client to use for RPC
|
||||
Client client.Client
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{
|
||||
Encoder: json.NewEncoder(),
|
||||
Context: context.Background(),
|
||||
Client: client.DefaultClient,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// WithEncoder sets the source encoder.
|
||||
func WithEncoder(e encoder.Encoder) Option {
|
||||
return func(o *Options) {
|
||||
o.Encoder = e
|
||||
}
|
||||
}
|
||||
|
||||
// WithClient sets the source client.
|
||||
func WithClient(c client.Client) Option {
|
||||
return func(o *Options) {
|
||||
o.Client = c
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Package source is the interface for sources
|
||||
package source
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrWatcherStopped is returned when source watcher has been stopped.
|
||||
ErrWatcherStopped = errors.New("watcher stopped")
|
||||
)
|
||||
|
||||
// Source is the source from which config is loaded.
|
||||
type Source interface {
|
||||
Read() (*ChangeSet, error)
|
||||
Write(*ChangeSet) error
|
||||
Watch() (Watcher, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
// ChangeSet represents a set of changes from a source.
|
||||
type ChangeSet struct {
|
||||
Timestamp time.Time
|
||||
Checksum string
|
||||
Format string
|
||||
Source string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// Watcher watches a source for changes.
|
||||
type Watcher interface {
|
||||
Next() (*ChangeSet, error)
|
||||
Stop() error
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v4/config/reader"
|
||||
)
|
||||
|
||||
type value struct{}
|
||||
|
||||
func newValue() reader.Value {
|
||||
return new(value)
|
||||
}
|
||||
|
||||
func (v *value) Bool(def bool) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (v *value) Int(def int) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (v *value) String(def string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (v *value) Float64(def float64) float64 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
func (v *value) Duration(def time.Duration) time.Duration {
|
||||
return time.Duration(0)
|
||||
}
|
||||
|
||||
func (v *value) StringSlice(def []string) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *value) StringMap(def map[string]string) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (v *value) Scan(val interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *value) Bytes() []byte {
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user