api: extract routers to external repos
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
parent
8a2b122015
commit
7c311aea19
@ -1,498 +0,0 @@
|
|||||||
// Package registry provides a dynamic api service router
|
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/unistack-org/micro/v3/api"
|
|
||||||
"github.com/unistack-org/micro/v3/api/router"
|
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
|
||||||
"github.com/unistack-org/micro/v3/metadata"
|
|
||||||
"github.com/unistack-org/micro/v3/registry"
|
|
||||||
util "github.com/unistack-org/micro/v3/util/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
// endpoint struct, that holds compiled pcre
|
|
||||||
type endpoint struct {
|
|
||||||
hostregs []*regexp.Regexp
|
|
||||||
pathregs []util.Pattern
|
|
||||||
pcreregs []*regexp.Regexp
|
|
||||||
}
|
|
||||||
|
|
||||||
// router is the default router
|
|
||||||
type registryRouter struct {
|
|
||||||
exit chan bool
|
|
||||||
opts router.Options
|
|
||||||
|
|
||||||
sync.RWMutex
|
|
||||||
eps map[string]*api.Service
|
|
||||||
// compiled regexp for host and path
|
|
||||||
ceps map[string]*endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *registryRouter) isClosed() bool {
|
|
||||||
select {
|
|
||||||
case <-r.exit:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// refresh list of api services
|
|
||||||
func (r *registryRouter) refresh() {
|
|
||||||
var attempts int
|
|
||||||
|
|
||||||
for {
|
|
||||||
services, err := r.opts.Registry.ListServices(r.opts.Context)
|
|
||||||
if err != nil {
|
|
||||||
attempts++
|
|
||||||
if logger.V(logger.ErrorLevel) {
|
|
||||||
logger.Errorf("unable to list services: %v", err)
|
|
||||||
}
|
|
||||||
time.Sleep(time.Duration(attempts) * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
attempts = 0
|
|
||||||
|
|
||||||
// for each service, get service and store endpoints
|
|
||||||
for _, s := range services {
|
|
||||||
service, err := r.opts.Registry.GetService(r.opts.Context, s.Name)
|
|
||||||
if err != nil {
|
|
||||||
if logger.V(logger.ErrorLevel) {
|
|
||||||
logger.Errorf("unable to get service: %v", err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r.store(service)
|
|
||||||
}
|
|
||||||
|
|
||||||
// refresh list in 10 minutes... cruft
|
|
||||||
// use registry watching
|
|
||||||
select {
|
|
||||||
case <-time.After(time.Minute * 10):
|
|
||||||
case <-r.exit:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// process watch event
|
|
||||||
func (r *registryRouter) process(res *registry.Result) {
|
|
||||||
// skip these things
|
|
||||||
if res == nil || res.Service == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get entry from cache
|
|
||||||
service, err := r.opts.Registry.GetService(r.opts.Context, res.Service.Name)
|
|
||||||
if err != nil {
|
|
||||||
if logger.V(logger.ErrorLevel) {
|
|
||||||
logger.Errorf("unable to get %v service: %v", res.Service.Name, err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// update our local endpoints
|
|
||||||
r.store(service)
|
|
||||||
}
|
|
||||||
|
|
||||||
// store local endpoint cache
|
|
||||||
func (r *registryRouter) store(services []*registry.Service) {
|
|
||||||
// endpoints
|
|
||||||
eps := map[string]*api.Service{}
|
|
||||||
|
|
||||||
// services
|
|
||||||
names := map[string]bool{}
|
|
||||||
|
|
||||||
// create a new endpoint mapping
|
|
||||||
for _, service := range services {
|
|
||||||
// set names we need later
|
|
||||||
names[service.Name] = true
|
|
||||||
|
|
||||||
// map per endpoint
|
|
||||||
for _, sep := range service.Endpoints {
|
|
||||||
// create a key service:endpoint_name
|
|
||||||
key := fmt.Sprintf("%s.%s", service.Name, sep.Name)
|
|
||||||
// decode endpoint
|
|
||||||
end := api.Decode(sep.Metadata)
|
|
||||||
// no endpoint or no name
|
|
||||||
if end == nil || len(end.Name) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// if we got nothing skip
|
|
||||||
if err := api.Validate(end); err != nil {
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Tracef("endpoint validation failed: %v", err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// try get endpoint
|
|
||||||
ep, ok := eps[key]
|
|
||||||
if !ok {
|
|
||||||
ep = &api.Service{Name: service.Name}
|
|
||||||
}
|
|
||||||
|
|
||||||
// overwrite the endpoint
|
|
||||||
ep.Endpoint = end
|
|
||||||
// append services
|
|
||||||
ep.Services = append(ep.Services, service)
|
|
||||||
// store it
|
|
||||||
eps[key] = ep
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Lock()
|
|
||||||
defer r.Unlock()
|
|
||||||
|
|
||||||
// delete any existing eps for services we know
|
|
||||||
for key, service := range r.eps {
|
|
||||||
// skip what we don't care about
|
|
||||||
if !names[service.Name] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// ok we know this thing
|
|
||||||
// delete delete delete
|
|
||||||
delete(r.eps, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// now set the eps we have
|
|
||||||
for name, ep := range eps {
|
|
||||||
r.eps[name] = ep
|
|
||||||
cep := &endpoint{}
|
|
||||||
|
|
||||||
for _, h := range ep.Endpoint.Host {
|
|
||||||
if h == "" || h == "*" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
hostreg, err := regexp.CompilePOSIX(h)
|
|
||||||
if err != nil {
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Tracef("endpoint have invalid host regexp: %v", err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cep.hostregs = append(cep.hostregs, hostreg)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range ep.Endpoint.Path {
|
|
||||||
var pcreok bool
|
|
||||||
|
|
||||||
if p[0] == '^' && p[len(p)-1] == '$' {
|
|
||||||
pcrereg, err := regexp.CompilePOSIX(p)
|
|
||||||
if err == nil {
|
|
||||||
cep.pcreregs = append(cep.pcreregs, pcrereg)
|
|
||||||
pcreok = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rule, err := util.Parse(p)
|
|
||||||
if err != nil && !pcreok {
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Tracef("endpoint have invalid path pattern: %v", err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
} else if err != nil && pcreok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := rule.Compile()
|
|
||||||
pathreg, err := util.NewPattern(tpl.Version, tpl.OpCodes, tpl.Pool, "")
|
|
||||||
if err != nil {
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Tracef("endpoint have invalid path pattern: %v", err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cep.pathregs = append(cep.pathregs, pathreg)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.ceps[name] = cep
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// watch for endpoint changes
|
|
||||||
func (r *registryRouter) watch() {
|
|
||||||
var attempts int
|
|
||||||
|
|
||||||
for {
|
|
||||||
if r.isClosed() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// watch for changes
|
|
||||||
w, err := r.opts.Registry.Watch(r.opts.Context)
|
|
||||||
if err != nil {
|
|
||||||
attempts++
|
|
||||||
if logger.V(logger.ErrorLevel) {
|
|
||||||
logger.Errorf("error watching endpoints: %v", err)
|
|
||||||
}
|
|
||||||
time.Sleep(time.Duration(attempts) * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ch := make(chan bool)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-ch:
|
|
||||||
w.Stop()
|
|
||||||
case <-r.exit:
|
|
||||||
w.Stop()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// reset if we get here
|
|
||||||
attempts = 0
|
|
||||||
|
|
||||||
for {
|
|
||||||
// process next event
|
|
||||||
res, err := w.Next()
|
|
||||||
if err != nil {
|
|
||||||
if logger.V(logger.ErrorLevel) {
|
|
||||||
logger.Errorf("error getting next endpoint: %v", err)
|
|
||||||
}
|
|
||||||
close(ch)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
r.process(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *registryRouter) Options() router.Options {
|
|
||||||
return r.opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *registryRouter) Close() error {
|
|
||||||
select {
|
|
||||||
case <-r.exit:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
close(r.exit)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *registryRouter) Register(ep *api.Endpoint) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *registryRouter) Deregister(ep *api.Endpoint) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *registryRouter) Endpoint(req *http.Request) (*api.Service, error) {
|
|
||||||
if r.isClosed() {
|
|
||||||
return nil, errors.New("router closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
r.RLock()
|
|
||||||
defer r.RUnlock()
|
|
||||||
|
|
||||||
var idx int
|
|
||||||
if len(req.URL.Path) > 0 && req.URL.Path != "/" {
|
|
||||||
idx = 1
|
|
||||||
}
|
|
||||||
path := strings.Split(req.URL.Path[idx:], "/")
|
|
||||||
|
|
||||||
// use the first match
|
|
||||||
// TODO: weighted matching
|
|
||||||
for n, e := range r.eps {
|
|
||||||
cep, ok := r.ceps[n]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ep := e.Endpoint
|
|
||||||
var mMatch, hMatch, pMatch bool
|
|
||||||
// 1. try method
|
|
||||||
for _, m := range ep.Method {
|
|
||||||
if m == req.Method {
|
|
||||||
mMatch = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !mMatch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Tracef("api method match %s", req.Method)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. try host
|
|
||||||
if len(ep.Host) == 0 {
|
|
||||||
hMatch = true
|
|
||||||
} else {
|
|
||||||
for idx, h := range ep.Host {
|
|
||||||
if h == "" || h == "*" {
|
|
||||||
hMatch = true
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
if cep.hostregs[idx].MatchString(req.URL.Host) {
|
|
||||||
hMatch = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hMatch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Tracef("api host match %s", req.URL.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. try path via google.api path matching
|
|
||||||
for _, pathreg := range cep.pathregs {
|
|
||||||
matches, err := pathreg.Match(path, "")
|
|
||||||
if err != nil {
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Tracef("api gpath not match %s != %v", path, pathreg)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Tracef("api gpath match %s = %v", path, pathreg)
|
|
||||||
}
|
|
||||||
pMatch = true
|
|
||||||
ctx := req.Context()
|
|
||||||
md, ok := metadata.FromContext(ctx)
|
|
||||||
if !ok {
|
|
||||||
md = make(metadata.Metadata)
|
|
||||||
}
|
|
||||||
for k, v := range matches {
|
|
||||||
md[fmt.Sprintf("x-api-field-%s", k)] = v
|
|
||||||
}
|
|
||||||
md["x-api-body"] = ep.Body
|
|
||||||
*req = *req.Clone(metadata.NewContext(ctx, md))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if !pMatch {
|
|
||||||
// 4. try path via pcre path matching
|
|
||||||
for _, pathreg := range cep.pcreregs {
|
|
||||||
if !pathreg.MatchString(req.URL.Path) {
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Tracef("api pcre path not match %s != %v", path, pathreg)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Tracef("api pcre path match %s != %v", path, pathreg)
|
|
||||||
}
|
|
||||||
pMatch = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !pMatch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Percentage traffic
|
|
||||||
// we got here, so its a match
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// no match
|
|
||||||
return nil, errors.New("not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *registryRouter) Route(req *http.Request) (*api.Service, error) {
|
|
||||||
if r.isClosed() {
|
|
||||||
return nil, errors.New("router closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// try get an endpoint
|
|
||||||
ep, err := r.Endpoint(req)
|
|
||||||
if err == nil {
|
|
||||||
return ep, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// error not nil
|
|
||||||
// ignore that shit
|
|
||||||
// TODO: don't ignore that shit
|
|
||||||
|
|
||||||
// get the service name
|
|
||||||
rp, err := r.opts.Resolver.Resolve(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// service name
|
|
||||||
name := rp.Name
|
|
||||||
|
|
||||||
// get service
|
|
||||||
services, err := r.opts.Registry.GetService(r.opts.Context, name, registry.GetDomain(rp.Domain))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// only use endpoint matching when the meta handler is set aka api.Default
|
|
||||||
switch r.opts.Handler {
|
|
||||||
// rpc handlers
|
|
||||||
case "meta", "api", "rpc":
|
|
||||||
handler := r.opts.Handler
|
|
||||||
|
|
||||||
// set default handler to api
|
|
||||||
if r.opts.Handler == "meta" {
|
|
||||||
handler = "rpc"
|
|
||||||
}
|
|
||||||
|
|
||||||
// construct api service
|
|
||||||
return &api.Service{
|
|
||||||
Name: name,
|
|
||||||
Endpoint: &api.Endpoint{
|
|
||||||
Name: rp.Method,
|
|
||||||
Handler: handler,
|
|
||||||
},
|
|
||||||
Services: services,
|
|
||||||
}, nil
|
|
||||||
// http handler
|
|
||||||
case "http", "proxy", "web":
|
|
||||||
// construct api service
|
|
||||||
return &api.Service{
|
|
||||||
Name: name,
|
|
||||||
Endpoint: &api.Endpoint{
|
|
||||||
Name: req.URL.String(),
|
|
||||||
Handler: r.opts.Handler,
|
|
||||||
Host: []string{req.Host},
|
|
||||||
Method: []string{req.Method},
|
|
||||||
Path: []string{req.URL.Path},
|
|
||||||
},
|
|
||||||
Services: services,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.New("unknown handler")
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRouter(opts ...router.Option) (*registryRouter, error) {
|
|
||||||
options := router.NewOptions(opts...)
|
|
||||||
if options.Registry == nil {
|
|
||||||
return nil, fmt.Errorf("registry is not set")
|
|
||||||
}
|
|
||||||
r := ®istryRouter{
|
|
||||||
exit: make(chan bool),
|
|
||||||
opts: options,
|
|
||||||
eps: make(map[string]*api.Service),
|
|
||||||
ceps: make(map[string]*endpoint),
|
|
||||||
}
|
|
||||||
go r.watch()
|
|
||||||
go r.refresh()
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRouter returns the default router
|
|
||||||
func NewRouter(opts ...router.Option) (router.Router, error) {
|
|
||||||
return newRouter(opts...)
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/unistack-org/micro/v3/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStoreRegex(t *testing.T) {
|
|
||||||
t.Skip()
|
|
||||||
router, err := newRouter()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
router.store([]*registry.Service{
|
|
||||||
{
|
|
||||||
Name: "Foobar",
|
|
||||||
Version: "latest",
|
|
||||||
Endpoints: []*registry.Endpoint{
|
|
||||||
{
|
|
||||||
Name: "foo",
|
|
||||||
Metadata: map[string]string{
|
|
||||||
"endpoint": "FooEndpoint",
|
|
||||||
"description": "Some description",
|
|
||||||
"method": "POST",
|
|
||||||
"path": "^/foo/$",
|
|
||||||
"handler": "rpc",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Metadata: map[string]string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.Len(t, router.ceps["Foobar.foo"].pcreregs, 1)
|
|
||||||
}
|
|
@ -1,257 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package router_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/unistack-org/micro/v3/api"
|
|
||||||
"github.com/unistack-org/micro/v3/api/handler"
|
|
||||||
"github.com/unistack-org/micro/v3/api/handler/rpc"
|
|
||||||
"github.com/unistack-org/micro/v3/api/router"
|
|
||||||
rregistry "github.com/unistack-org/micro/v3/api/router/registry"
|
|
||||||
rstatic "github.com/unistack-org/micro/v3/api/router/static"
|
|
||||||
"github.com/unistack-org/micro/v3/broker"
|
|
||||||
bmemory "github.com/unistack-org/micro/v3/broker/memory"
|
|
||||||
"github.com/unistack-org/micro/v3/client"
|
|
||||||
gcli "github.com/unistack-org/micro/v3/client/grpc"
|
|
||||||
rmemory "github.com/unistack-org/micro/v3/registry/memory"
|
|
||||||
rt "github.com/unistack-org/micro/v3/router"
|
|
||||||
regRouter "github.com/unistack-org/micro/v3/router/registry"
|
|
||||||
"github.com/unistack-org/micro/v3/server"
|
|
||||||
gsrv "github.com/unistack-org/micro/v3/server/grpc"
|
|
||||||
pb "github.com/unistack-org/micro/v3/server/grpc/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// server is used to implement helloworld.GreeterServer.
|
|
||||||
type testServer struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHello implements helloworld.GreeterServer
|
|
||||||
func (s *testServer) Call(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
|
|
||||||
rsp.Msg = "Hello " + req.Uuid
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHello implements helloworld.GreeterServer
|
|
||||||
func (s *testServer) CallPcre(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
|
|
||||||
rsp.Msg = "Hello " + req.Uuid
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHello implements helloworld.GreeterServer
|
|
||||||
func (s *testServer) CallPcreInvalid(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
|
|
||||||
rsp.Msg = "Hello " + req.Uuid
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initial(t *testing.T) (server.Server, client.Client) {
|
|
||||||
r := rmemory.NewRegistry()
|
|
||||||
b := bmemory.NewBroker(broker.Registry(r))
|
|
||||||
|
|
||||||
// create a new client
|
|
||||||
s := gsrv.NewServer(
|
|
||||||
server.Name("foo"),
|
|
||||||
server.Broker(b),
|
|
||||||
server.Registry(r),
|
|
||||||
)
|
|
||||||
|
|
||||||
rtr := regRouter.NewRouter(
|
|
||||||
rt.Registry(r),
|
|
||||||
)
|
|
||||||
|
|
||||||
// create a new server
|
|
||||||
c := gcli.NewClient(
|
|
||||||
client.Router(rtr),
|
|
||||||
client.Broker(b),
|
|
||||||
)
|
|
||||||
|
|
||||||
h := &testServer{}
|
|
||||||
pb.RegisterTestHandler(s, h)
|
|
||||||
|
|
||||||
if err := s.Start(); err != nil {
|
|
||||||
t.Fatalf("failed to start: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return s, c
|
|
||||||
}
|
|
||||||
|
|
||||||
func check(t *testing.T, addr string, path string, expected string) {
|
|
||||||
req, err := http.NewRequest("POST", fmt.Sprintf(path, addr), nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to created http.Request: %v", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
rsp, err := (&http.Client{}).Do(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to created http.Request: %v", err)
|
|
||||||
}
|
|
||||||
defer rsp.Body.Close()
|
|
||||||
|
|
||||||
buf, err := ioutil.ReadAll(rsp.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonMsg := expected
|
|
||||||
if string(buf) != jsonMsg {
|
|
||||||
t.Fatalf("invalid message received, parsing error %s != %s", buf, jsonMsg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRouterRegistryPcre(t *testing.T) {
|
|
||||||
s, c := initial(t)
|
|
||||||
defer s.Stop()
|
|
||||||
|
|
||||||
router := rregistry.NewRouter(
|
|
||||||
router.WithHandler(rpc.Handler),
|
|
||||||
router.WithRegistry(s.Options().Registry),
|
|
||||||
)
|
|
||||||
hrpc := rpc.NewHandler(
|
|
||||||
handler.WithClient(c),
|
|
||||||
handler.WithRouter(router),
|
|
||||||
)
|
|
||||||
hsrv := &http.Server{
|
|
||||||
Handler: hrpc,
|
|
||||||
Addr: "127.0.0.1:6543",
|
|
||||||
WriteTimeout: 15 * time.Second,
|
|
||||||
ReadTimeout: 15 * time.Second,
|
|
||||||
IdleTimeout: 20 * time.Second,
|
|
||||||
MaxHeaderBytes: 1024 * 1024 * 1, // 1Mb
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
log.Println(hsrv.ListenAndServe())
|
|
||||||
}()
|
|
||||||
|
|
||||||
defer hsrv.Close()
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
check(t, hsrv.Addr, "http://%s/api/v0/test/call/TEST", `{"msg":"Hello TEST"}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRouterStaticPcre(t *testing.T) {
|
|
||||||
s, c := initial(t)
|
|
||||||
defer s.Stop()
|
|
||||||
|
|
||||||
router := rstatic.NewRouter(
|
|
||||||
router.WithHandler(rpc.Handler),
|
|
||||||
router.WithRegistry(s.Options().Registry),
|
|
||||||
)
|
|
||||||
|
|
||||||
err := router.Register(&api.Endpoint{
|
|
||||||
Name: "foo.Test.Call",
|
|
||||||
Method: []string{"POST"},
|
|
||||||
Path: []string{"^/api/v0/test/call/?$"},
|
|
||||||
Handler: "rpc",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hrpc := rpc.NewHandler(
|
|
||||||
handler.WithClient(c),
|
|
||||||
handler.WithRouter(router),
|
|
||||||
)
|
|
||||||
hsrv := &http.Server{
|
|
||||||
Handler: hrpc,
|
|
||||||
Addr: "127.0.0.1:6543",
|
|
||||||
WriteTimeout: 15 * time.Second,
|
|
||||||
ReadTimeout: 15 * time.Second,
|
|
||||||
IdleTimeout: 20 * time.Second,
|
|
||||||
MaxHeaderBytes: 1024 * 1024 * 1, // 1Mb
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
log.Println(hsrv.ListenAndServe())
|
|
||||||
}()
|
|
||||||
defer hsrv.Close()
|
|
||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
check(t, hsrv.Addr, "http://%s/api/v0/test/call", `{"msg":"Hello "}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRouterStaticGpath(t *testing.T) {
|
|
||||||
s, c := initial(t)
|
|
||||||
defer s.Stop()
|
|
||||||
|
|
||||||
router := rstatic.NewRouter(
|
|
||||||
router.WithHandler(rpc.Handler),
|
|
||||||
router.WithRegistry(s.Options().Registry),
|
|
||||||
)
|
|
||||||
|
|
||||||
err := router.Register(&api.Endpoint{
|
|
||||||
Name: "foo.Test.Call",
|
|
||||||
Method: []string{"POST"},
|
|
||||||
Path: []string{"/api/v0/test/call/{uuid}"},
|
|
||||||
Handler: "rpc",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hrpc := rpc.NewHandler(
|
|
||||||
handler.WithClient(c),
|
|
||||||
handler.WithRouter(router),
|
|
||||||
)
|
|
||||||
hsrv := &http.Server{
|
|
||||||
Handler: hrpc,
|
|
||||||
Addr: "127.0.0.1:6543",
|
|
||||||
WriteTimeout: 15 * time.Second,
|
|
||||||
ReadTimeout: 15 * time.Second,
|
|
||||||
IdleTimeout: 20 * time.Second,
|
|
||||||
MaxHeaderBytes: 1024 * 1024 * 1, // 1Mb
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
log.Println(hsrv.ListenAndServe())
|
|
||||||
}()
|
|
||||||
defer hsrv.Close()
|
|
||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
check(t, hsrv.Addr, "http://%s/api/v0/test/call/TEST", `{"msg":"Hello TEST"}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRouterStaticPcreInvalid(t *testing.T) {
|
|
||||||
var ep *api.Endpoint
|
|
||||||
var err error
|
|
||||||
|
|
||||||
s, c := initial(t)
|
|
||||||
defer s.Stop()
|
|
||||||
|
|
||||||
router := rstatic.NewRouter(
|
|
||||||
router.WithHandler(rpc.Handler),
|
|
||||||
router.WithRegistry(s.Options().Registry),
|
|
||||||
)
|
|
||||||
|
|
||||||
ep = &api.Endpoint{
|
|
||||||
Name: "foo.Test.Call",
|
|
||||||
Method: []string{"POST"},
|
|
||||||
Path: []string{"^/api/v0/test/call/?"},
|
|
||||||
Handler: "rpc",
|
|
||||||
}
|
|
||||||
|
|
||||||
err = router.Register(ep)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("invalid endpoint %v", ep)
|
|
||||||
}
|
|
||||||
|
|
||||||
ep = &api.Endpoint{
|
|
||||||
Name: "foo.Test.Call",
|
|
||||||
Method: []string{"POST"},
|
|
||||||
Path: []string{"/api/v0/test/call/?$"},
|
|
||||||
Handler: "rpc",
|
|
||||||
}
|
|
||||||
|
|
||||||
err = router.Register(ep)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("invalid endpoint %v", ep)
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = c
|
|
||||||
}
|
|
@ -1,356 +0,0 @@
|
|||||||
package static
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/unistack-org/micro/v3/api"
|
|
||||||
"github.com/unistack-org/micro/v3/api/router"
|
|
||||||
"github.com/unistack-org/micro/v3/logger"
|
|
||||||
"github.com/unistack-org/micro/v3/metadata"
|
|
||||||
"github.com/unistack-org/micro/v3/registry"
|
|
||||||
rutil "github.com/unistack-org/micro/v3/util/registry"
|
|
||||||
util "github.com/unistack-org/micro/v3/util/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
type endpoint struct {
|
|
||||||
apiep *api.Endpoint
|
|
||||||
hostregs []*regexp.Regexp
|
|
||||||
pathregs []util.Pattern
|
|
||||||
pcreregs []*regexp.Regexp
|
|
||||||
}
|
|
||||||
|
|
||||||
// router is the default router
|
|
||||||
type staticRouter struct {
|
|
||||||
exit chan bool
|
|
||||||
opts router.Options
|
|
||||||
sync.RWMutex
|
|
||||||
eps map[string]*endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *staticRouter) isClosed() bool {
|
|
||||||
select {
|
|
||||||
case <-r.exit:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
// watch for endpoint changes
|
|
||||||
func (r *staticRouter) watch() {
|
|
||||||
var attempts int
|
|
||||||
|
|
||||||
for {
|
|
||||||
if r.isClosed() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// watch for changes
|
|
||||||
w, err := r.opts.Registry.Watch()
|
|
||||||
if err != nil {
|
|
||||||
attempts++
|
|
||||||
log.Println("Error watching endpoints", err)
|
|
||||||
time.Sleep(time.Duration(attempts) * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ch := make(chan bool)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-ch:
|
|
||||||
w.Stop()
|
|
||||||
case <-r.exit:
|
|
||||||
w.Stop()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// reset if we get here
|
|
||||||
attempts = 0
|
|
||||||
|
|
||||||
for {
|
|
||||||
// process next event
|
|
||||||
res, err := w.Next()
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Error getting next endpoint", err)
|
|
||||||
close(ch)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
r.process(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
func (r *staticRouter) Register(ep *api.Endpoint) error {
|
|
||||||
if err := api.Validate(ep); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var pathregs []util.Pattern
|
|
||||||
var hostregs []*regexp.Regexp
|
|
||||||
var pcreregs []*regexp.Regexp
|
|
||||||
|
|
||||||
for _, h := range ep.Host {
|
|
||||||
if h == "" || h == "*" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
hostreg, err := regexp.CompilePOSIX(h)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
hostregs = append(hostregs, hostreg)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range ep.Path {
|
|
||||||
var pcreok bool
|
|
||||||
|
|
||||||
// pcre only when we have start and end markers
|
|
||||||
if p[0] == '^' && p[len(p)-1] == '$' {
|
|
||||||
pcrereg, err := regexp.CompilePOSIX(p)
|
|
||||||
if err == nil {
|
|
||||||
pcreregs = append(pcreregs, pcrereg)
|
|
||||||
pcreok = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rule, err := util.Parse(p)
|
|
||||||
if err != nil && !pcreok {
|
|
||||||
return err
|
|
||||||
} else if err != nil && pcreok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := rule.Compile()
|
|
||||||
pathreg, err := util.NewPattern(tpl.Version, tpl.OpCodes, tpl.Pool, "")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pathregs = append(pathregs, pathreg)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Lock()
|
|
||||||
r.eps[ep.Name] = &endpoint{
|
|
||||||
apiep: ep,
|
|
||||||
pcreregs: pcreregs,
|
|
||||||
pathregs: pathregs,
|
|
||||||
hostregs: hostregs,
|
|
||||||
}
|
|
||||||
r.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *staticRouter) Deregister(ep *api.Endpoint) error {
|
|
||||||
if err := api.Validate(ep); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
r.Lock()
|
|
||||||
delete(r.eps, ep.Name)
|
|
||||||
r.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *staticRouter) Options() router.Options {
|
|
||||||
return r.opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *staticRouter) Close() error {
|
|
||||||
select {
|
|
||||||
case <-r.exit:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
close(r.exit)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *staticRouter) Endpoint(req *http.Request) (*api.Service, error) {
|
|
||||||
ep, err := r.endpoint(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
epf := strings.Split(ep.apiep.Name, ".")
|
|
||||||
services, err := r.opts.Registry.GetService(r.opts.Context, epf[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// hack for stream endpoint
|
|
||||||
if ep.apiep.Stream {
|
|
||||||
svcs := rutil.Copy(services)
|
|
||||||
for _, svc := range svcs {
|
|
||||||
if len(svc.Endpoints) == 0 {
|
|
||||||
e := ®istry.Endpoint{}
|
|
||||||
e.Name = strings.Join(epf[1:], ".")
|
|
||||||
e.Metadata = make(map[string]string)
|
|
||||||
e.Metadata["stream"] = "true"
|
|
||||||
svc.Endpoints = append(svc.Endpoints, e)
|
|
||||||
}
|
|
||||||
for _, e := range svc.Endpoints {
|
|
||||||
e.Name = strings.Join(epf[1:], ".")
|
|
||||||
e.Metadata = make(map[string]string)
|
|
||||||
e.Metadata["stream"] = "true"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
services = svcs
|
|
||||||
}
|
|
||||||
|
|
||||||
svc := &api.Service{
|
|
||||||
Name: epf[0],
|
|
||||||
Endpoint: &api.Endpoint{
|
|
||||||
Name: strings.Join(epf[1:], "."),
|
|
||||||
Handler: "rpc",
|
|
||||||
Host: ep.apiep.Host,
|
|
||||||
Method: ep.apiep.Method,
|
|
||||||
Path: ep.apiep.Path,
|
|
||||||
Body: ep.apiep.Body,
|
|
||||||
Stream: ep.apiep.Stream,
|
|
||||||
},
|
|
||||||
Services: services,
|
|
||||||
}
|
|
||||||
|
|
||||||
return svc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *staticRouter) endpoint(req *http.Request) (*endpoint, error) {
|
|
||||||
if r.isClosed() {
|
|
||||||
return nil, errors.New("router closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
r.RLock()
|
|
||||||
defer r.RUnlock()
|
|
||||||
|
|
||||||
var idx int
|
|
||||||
if len(req.URL.Path) > 0 && req.URL.Path != "/" {
|
|
||||||
idx = 1
|
|
||||||
}
|
|
||||||
path := strings.Split(req.URL.Path[idx:], "/")
|
|
||||||
// use the first match
|
|
||||||
// TODO: weighted matching
|
|
||||||
|
|
||||||
for _, ep := range r.eps {
|
|
||||||
var mMatch, hMatch, pMatch bool
|
|
||||||
|
|
||||||
// 1. try method
|
|
||||||
for _, m := range ep.apiep.Method {
|
|
||||||
if m == req.Method {
|
|
||||||
mMatch = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !mMatch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Tracef("api method match %s", req.Method)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. try host
|
|
||||||
if len(ep.apiep.Host) == 0 {
|
|
||||||
hMatch = true
|
|
||||||
} else {
|
|
||||||
for idx, h := range ep.apiep.Host {
|
|
||||||
if h == "" || h == "*" {
|
|
||||||
hMatch = true
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
if ep.hostregs[idx].MatchString(req.URL.Host) {
|
|
||||||
hMatch = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hMatch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Tracef("api host match %s", req.URL.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. try google.api path
|
|
||||||
for _, pathreg := range ep.pathregs {
|
|
||||||
matches, err := pathreg.Match(path, "")
|
|
||||||
if err != nil {
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Tracef("api gpath not match %s != %v", path, pathreg)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Tracef("api gpath match %s = %v", path, pathreg)
|
|
||||||
}
|
|
||||||
pMatch = true
|
|
||||||
ctx := req.Context()
|
|
||||||
md, ok := metadata.FromContext(ctx)
|
|
||||||
if !ok {
|
|
||||||
md = make(metadata.Metadata)
|
|
||||||
}
|
|
||||||
for k, v := range matches {
|
|
||||||
md[fmt.Sprintf("x-api-field-%s", k)] = v
|
|
||||||
}
|
|
||||||
md["x-api-body"] = ep.apiep.Body
|
|
||||||
*req = *req.Clone(metadata.NewContext(ctx, md))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if !pMatch {
|
|
||||||
// 4. try path via pcre path matching
|
|
||||||
for _, pathreg := range ep.pcreregs {
|
|
||||||
if !pathreg.MatchString(req.URL.Path) {
|
|
||||||
if logger.V(logger.TraceLevel) {
|
|
||||||
logger.Tracef("api pcre path not match %s != %v", req.URL.Path, pathreg)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pMatch = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !pMatch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// TODO: Percentage traffic
|
|
||||||
|
|
||||||
// we got here, so its a match
|
|
||||||
return ep, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// no match
|
|
||||||
return nil, fmt.Errorf("endpoint not found for %v", req.URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *staticRouter) Route(req *http.Request) (*api.Service, error) {
|
|
||||||
if r.isClosed() {
|
|
||||||
return nil, errors.New("router closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// try get an endpoint
|
|
||||||
ep, err := r.Endpoint(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ep, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRouter(opts ...router.Option) *staticRouter {
|
|
||||||
options := router.NewOptions(opts...)
|
|
||||||
r := &staticRouter{
|
|
||||||
exit: make(chan bool),
|
|
||||||
opts: options,
|
|
||||||
eps: make(map[string]*endpoint),
|
|
||||||
}
|
|
||||||
//go r.watch()
|
|
||||||
//go r.refresh()
|
|
||||||
return r
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user