2019-06-03 18:44:43 +01:00
|
|
|
// Package registry provides a dynamic api service router
|
|
|
|
package registry
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"regexp"
|
2020-04-15 17:50:51 +03:00
|
|
|
"strings"
|
2019-06-03 18:44:43 +01:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
2020-08-19 17:47:17 +03:00
|
|
|
"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"
|
2019-06-03 18:44:43 +01:00
|
|
|
)
|
|
|
|
|
2020-04-15 17:50:51 +03:00
|
|
|
// endpoint struct, that holds compiled pcre
|
|
|
|
type endpoint struct {
|
|
|
|
hostregs []*regexp.Regexp
|
|
|
|
pathregs []util.Pattern
|
2020-04-19 00:31:34 +03:00
|
|
|
pcreregs []*regexp.Regexp
|
2020-04-15 17:50:51 +03:00
|
|
|
}
|
|
|
|
|
2019-06-03 18:44:43 +01:00
|
|
|
// router is the default router
|
|
|
|
type registryRouter struct {
|
|
|
|
exit chan bool
|
|
|
|
opts router.Options
|
|
|
|
|
|
|
|
sync.RWMutex
|
|
|
|
eps map[string]*api.Service
|
2020-04-15 17:50:51 +03:00
|
|
|
// compiled regexp for host and path
|
|
|
|
ceps map[string]*endpoint
|
2019-06-03 18:44:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r *registryRouter) isClosed() bool {
|
|
|
|
select {
|
|
|
|
case <-r.exit:
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// refresh list of api services
|
|
|
|
func (r *registryRouter) refresh() {
|
|
|
|
var attempts int
|
|
|
|
|
|
|
|
for {
|
|
|
|
services, err := r.opts.Registry.ListServices()
|
|
|
|
if err != nil {
|
|
|
|
attempts++
|
2020-09-05 02:11:29 +03:00
|
|
|
if logger.V(logger.ErrorLevel) {
|
2020-03-24 23:45:11 +03:00
|
|
|
logger.Errorf("unable to list services: %v", err)
|
|
|
|
}
|
2019-06-03 18:44:43 +01:00
|
|
|
time.Sleep(time.Duration(attempts) * time.Second)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
attempts = 0
|
|
|
|
|
|
|
|
// for each service, get service and store endpoints
|
|
|
|
for _, s := range services {
|
2020-08-25 13:44:41 +03:00
|
|
|
service, err := r.opts.Registry.GetService(s.Name)
|
2019-06-03 18:44:43 +01:00
|
|
|
if err != nil {
|
2020-09-05 02:11:29 +03:00
|
|
|
if logger.V(logger.ErrorLevel) {
|
2020-03-24 23:45:11 +03:00
|
|
|
logger.Errorf("unable to get service: %v", err)
|
|
|
|
}
|
2019-06-03 18:44:43 +01:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
r.store(service)
|
|
|
|
}
|
|
|
|
|
|
|
|
// refresh list in 10 minutes... cruft
|
2020-04-15 17:50:51 +03:00
|
|
|
// use registry watching
|
2019-06-03 18:44:43 +01:00
|
|
|
select {
|
|
|
|
case <-time.After(time.Minute * 10):
|
|
|
|
case <-r.exit:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// process watch event
|
|
|
|
func (r *registryRouter) process(res *registry.Result) {
|
|
|
|
// skip these things
|
2020-04-08 15:38:02 +01:00
|
|
|
if res == nil || res.Service == nil {
|
2019-06-03 18:44:43 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// get entry from cache
|
2020-08-25 13:44:41 +03:00
|
|
|
service, err := r.opts.Registry.GetService(res.Service.Name)
|
2019-06-03 18:44:43 +01:00
|
|
|
if err != nil {
|
2020-09-05 02:11:29 +03:00
|
|
|
if logger.V(logger.ErrorLevel) {
|
2020-06-26 14:28:18 +01:00
|
|
|
logger.Errorf("unable to get %v service: %v", res.Service.Name, err)
|
2020-03-24 23:45:11 +03:00
|
|
|
}
|
2019-06-03 18:44:43 +01:00
|
|
|
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
|
2020-04-15 17:50:51 +03:00
|
|
|
for _, sep := range service.Endpoints {
|
2019-06-03 18:44:43 +01:00
|
|
|
// create a key service:endpoint_name
|
2020-04-15 17:50:51 +03:00
|
|
|
key := fmt.Sprintf("%s.%s", service.Name, sep.Name)
|
2019-06-03 18:44:43 +01:00
|
|
|
// decode endpoint
|
2020-04-15 17:50:51 +03:00
|
|
|
end := api.Decode(sep.Metadata)
|
2020-08-05 18:09:04 +01:00
|
|
|
// no endpoint or no name
|
|
|
|
if end == nil || len(end.Name) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
2019-06-03 18:44:43 +01:00
|
|
|
// if we got nothing skip
|
|
|
|
if err := api.Validate(end); err != nil {
|
2020-09-05 02:11:29 +03:00
|
|
|
if logger.V(logger.TraceLevel) {
|
2020-03-27 14:01:47 +00:00
|
|
|
logger.Tracef("endpoint validation failed: %v", err)
|
2020-03-24 23:45:11 +03:00
|
|
|
}
|
2019-06-03 18:44:43 +01:00
|
|
|
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
|
2020-04-15 17:50:51 +03:00
|
|
|
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 {
|
2020-09-05 02:11:29 +03:00
|
|
|
if logger.V(logger.TraceLevel) {
|
2020-04-15 17:50:51 +03:00
|
|
|
logger.Tracef("endpoint have invalid host regexp: %v", err)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
cep.hostregs = append(cep.hostregs, hostreg)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, p := range ep.Endpoint.Path {
|
2020-04-19 00:31:34 +03:00
|
|
|
var pcreok bool
|
2020-04-29 15:23:10 +03:00
|
|
|
|
2020-06-10 11:18:03 +01:00
|
|
|
if p[0] == '^' && p[len(p)-1] == '$' {
|
2020-04-29 15:23:10 +03:00
|
|
|
pcrereg, err := regexp.CompilePOSIX(p)
|
|
|
|
if err == nil {
|
|
|
|
cep.pcreregs = append(cep.pcreregs, pcrereg)
|
|
|
|
pcreok = true
|
|
|
|
}
|
2020-04-19 00:31:34 +03:00
|
|
|
}
|
|
|
|
|
2020-04-15 17:50:51 +03:00
|
|
|
rule, err := util.Parse(p)
|
2020-04-19 00:31:34 +03:00
|
|
|
if err != nil && !pcreok {
|
2020-09-05 02:11:29 +03:00
|
|
|
if logger.V(logger.TraceLevel) {
|
2020-04-15 17:50:51 +03:00
|
|
|
logger.Tracef("endpoint have invalid path pattern: %v", err)
|
|
|
|
}
|
|
|
|
continue
|
2020-04-19 00:31:34 +03:00
|
|
|
} else if err != nil && pcreok {
|
|
|
|
continue
|
2020-04-15 17:50:51 +03:00
|
|
|
}
|
2020-04-19 00:31:34 +03:00
|
|
|
|
2020-04-15 17:50:51 +03:00
|
|
|
tpl := rule.Compile()
|
|
|
|
pathreg, err := util.NewPattern(tpl.Version, tpl.OpCodes, tpl.Pool, "")
|
|
|
|
if err != nil {
|
2020-09-05 02:11:29 +03:00
|
|
|
if logger.V(logger.TraceLevel) {
|
2020-04-15 17:50:51 +03:00
|
|
|
logger.Tracef("endpoint have invalid path pattern: %v", err)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
cep.pathregs = append(cep.pathregs, pathreg)
|
|
|
|
}
|
|
|
|
|
|
|
|
r.ceps[name] = cep
|
2019-06-03 18:44:43 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// watch for endpoint changes
|
|
|
|
func (r *registryRouter) watch() {
|
|
|
|
var attempts int
|
|
|
|
|
|
|
|
for {
|
|
|
|
if r.isClosed() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// watch for changes
|
|
|
|
w, err := r.opts.Registry.Watch()
|
|
|
|
if err != nil {
|
|
|
|
attempts++
|
2020-09-05 02:11:29 +03:00
|
|
|
if logger.V(logger.ErrorLevel) {
|
2020-03-24 23:45:11 +03:00
|
|
|
logger.Errorf("error watching endpoints: %v", err)
|
|
|
|
}
|
2019-06-03 18:44:43 +01:00
|
|
|
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 {
|
2020-09-05 02:11:29 +03:00
|
|
|
if logger.V(logger.ErrorLevel) {
|
2020-07-06 13:52:42 +01:00
|
|
|
logger.Errorf("error getting next endpoint: %v", err)
|
2020-03-24 23:45:11 +03:00
|
|
|
}
|
2019-06-03 18:44:43 +01:00
|
|
|
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
|
2020-03-30 11:04:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r *registryRouter) Register(ep *api.Endpoint) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *registryRouter) Deregister(ep *api.Endpoint) error {
|
|
|
|
return nil
|
2019-06-03 18:44:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r *registryRouter) Endpoint(req *http.Request) (*api.Service, error) {
|
|
|
|
if r.isClosed() {
|
|
|
|
return nil, errors.New("router closed")
|
|
|
|
}
|
|
|
|
|
|
|
|
r.RLock()
|
|
|
|
defer r.RUnlock()
|
|
|
|
|
2020-04-15 17:50:51 +03:00
|
|
|
var idx int
|
|
|
|
if len(req.URL.Path) > 0 && req.URL.Path != "/" {
|
|
|
|
idx = 1
|
|
|
|
}
|
|
|
|
path := strings.Split(req.URL.Path[idx:], "/")
|
|
|
|
|
2019-06-03 18:44:43 +01:00
|
|
|
// use the first match
|
|
|
|
// TODO: weighted matching
|
2020-04-15 17:50:51 +03:00
|
|
|
for n, e := range r.eps {
|
|
|
|
cep, ok := r.ceps[n]
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
2019-06-03 18:44:43 +01:00
|
|
|
ep := e.Endpoint
|
2020-04-15 17:50:51 +03:00
|
|
|
var mMatch, hMatch, pMatch bool
|
|
|
|
// 1. try method
|
2019-06-03 18:44:43 +01:00
|
|
|
for _, m := range ep.Method {
|
2020-04-15 17:50:51 +03:00
|
|
|
if m == req.Method {
|
|
|
|
mMatch = true
|
2020-04-19 00:31:34 +03:00
|
|
|
break
|
2019-06-03 18:44:43 +01:00
|
|
|
}
|
|
|
|
}
|
2020-04-15 17:50:51 +03:00
|
|
|
if !mMatch {
|
2019-06-03 18:44:43 +01:00
|
|
|
continue
|
|
|
|
}
|
2020-09-05 02:43:16 +03:00
|
|
|
if logger.V(logger.TraceLevel) {
|
|
|
|
logger.Tracef("api method match %s", req.Method)
|
2020-04-15 17:50:51 +03:00
|
|
|
}
|
2019-06-03 18:44:43 +01:00
|
|
|
|
2020-04-15 17:50:51 +03:00
|
|
|
// 2. try host
|
|
|
|
if len(ep.Host) == 0 {
|
|
|
|
hMatch = true
|
|
|
|
} else {
|
|
|
|
for idx, h := range ep.Host {
|
|
|
|
if h == "" || h == "*" {
|
|
|
|
hMatch = true
|
2020-04-19 00:31:34 +03:00
|
|
|
break
|
2020-04-15 17:50:51 +03:00
|
|
|
} else {
|
|
|
|
if cep.hostregs[idx].MatchString(req.URL.Host) {
|
|
|
|
hMatch = true
|
2020-04-19 00:31:34 +03:00
|
|
|
break
|
2020-04-15 17:50:51 +03:00
|
|
|
}
|
|
|
|
}
|
2019-06-03 18:44:43 +01:00
|
|
|
}
|
|
|
|
}
|
2020-04-15 17:50:51 +03:00
|
|
|
if !hMatch {
|
2019-06-03 18:44:43 +01:00
|
|
|
continue
|
|
|
|
}
|
2020-09-05 02:43:16 +03:00
|
|
|
if logger.V(logger.TraceLevel) {
|
|
|
|
logger.Tracef("api host match %s", req.URL.Host)
|
2020-04-15 17:50:51 +03:00
|
|
|
}
|
2019-06-03 18:44:43 +01:00
|
|
|
|
2020-04-19 00:31:34 +03:00
|
|
|
// 3. try path via google.api path matching
|
2020-04-15 17:50:51 +03:00
|
|
|
for _, pathreg := range cep.pathregs {
|
|
|
|
matches, err := pathreg.Match(path, "")
|
|
|
|
if err != nil {
|
2020-09-05 02:43:16 +03:00
|
|
|
if logger.V(logger.TraceLevel) {
|
|
|
|
logger.Tracef("api gpath not match %s != %v", path, pathreg)
|
2020-04-15 17:50:51 +03:00
|
|
|
}
|
|
|
|
continue
|
2019-06-03 18:44:43 +01:00
|
|
|
}
|
2020-09-05 02:43:16 +03:00
|
|
|
if logger.V(logger.TraceLevel) {
|
|
|
|
logger.Tracef("api gpath match %s = %v", path, pathreg)
|
2020-04-29 15:23:10 +03:00
|
|
|
}
|
2020-04-15 17:50:51 +03:00
|
|
|
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))
|
2020-04-19 00:31:34 +03:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2020-04-29 15:23:10 +03:00
|
|
|
if !pMatch {
|
|
|
|
// 4. try path via pcre path matching
|
|
|
|
for _, pathreg := range cep.pcreregs {
|
|
|
|
if !pathreg.MatchString(req.URL.Path) {
|
2020-09-05 02:43:16 +03:00
|
|
|
if logger.V(logger.TraceLevel) {
|
|
|
|
logger.Tracef("api pcre path not match %s != %v", path, pathreg)
|
2020-04-29 15:23:10 +03:00
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
2020-09-05 02:43:16 +03:00
|
|
|
if logger.V(logger.TraceLevel) {
|
|
|
|
logger.Tracef("api pcre path match %s != %v", path, pathreg)
|
2020-04-19 00:31:34 +03:00
|
|
|
}
|
2020-04-29 15:23:10 +03:00
|
|
|
pMatch = true
|
|
|
|
break
|
2020-04-19 00:31:34 +03:00
|
|
|
}
|
2019-06-03 18:44:43 +01:00
|
|
|
}
|
2020-04-19 00:31:34 +03:00
|
|
|
|
2020-04-15 17:50:51 +03:00
|
|
|
if !pMatch {
|
2019-06-03 18:44:43 +01:00
|
|
|
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
|
2020-04-08 15:38:02 +01:00
|
|
|
name := rp.Name
|
2019-06-03 18:44:43 +01:00
|
|
|
|
|
|
|
// get service
|
2020-08-25 13:44:41 +03:00
|
|
|
services, err := r.opts.Registry.GetService(name, registry.GetDomain(rp.Domain))
|
2019-06-03 18:44:43 +01:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2020-08-27 11:32:27 +03:00
|
|
|
func newRouter(opts ...router.Option) (*registryRouter, error) {
|
2019-06-03 18:44:43 +01:00
|
|
|
options := router.NewOptions(opts...)
|
2020-08-27 11:32:27 +03:00
|
|
|
if options.Registry == nil {
|
|
|
|
return nil, fmt.Errorf("registry is not set")
|
|
|
|
}
|
2019-06-03 18:44:43 +01:00
|
|
|
r := ®istryRouter{
|
|
|
|
exit: make(chan bool),
|
|
|
|
opts: options,
|
|
|
|
eps: make(map[string]*api.Service),
|
2020-04-15 17:50:51 +03:00
|
|
|
ceps: make(map[string]*endpoint),
|
2019-06-03 18:44:43 +01:00
|
|
|
}
|
|
|
|
go r.watch()
|
|
|
|
go r.refresh()
|
2020-08-27 11:32:27 +03:00
|
|
|
return r, nil
|
2019-06-03 18:44:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewRouter returns the default router
|
2020-08-27 11:32:27 +03:00
|
|
|
func NewRouter(opts ...router.Option) (router.Router, error) {
|
2019-06-03 18:44:43 +01:00
|
|
|
return newRouter(opts...)
|
|
|
|
}
|