api/resolver: update resolver to enable subdomain routing (#1747)
* api/resolver: update domain / service prefix usage * api/resolver/subdomain: implement subdomain resolver for domain resolution * api/handler: fix tests
This commit is contained in:
parent
4f0f4326df
commit
104b7d8f8d
@ -58,7 +58,7 @@ func testHttp(t *testing.T, path, service, ns string) {
|
|||||||
router.WithHandler("http"),
|
router.WithHandler("http"),
|
||||||
router.WithRegistry(r),
|
router.WithRegistry(r),
|
||||||
router.WithResolver(vpath.NewResolver(
|
router.WithResolver(vpath.NewResolver(
|
||||||
resolver.WithNamespace(resolver.StaticNamespace(ns)),
|
resolver.WithServicePrefix(ns),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -9,7 +9,9 @@ import (
|
|||||||
"github.com/micro/go-micro/v2/api/resolver"
|
"github.com/micro/go-micro/v2/api/resolver"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Resolver struct{}
|
type Resolver struct {
|
||||||
|
opts resolver.Options
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
||||||
// /foo.Bar/Service
|
// /foo.Bar/Service
|
||||||
@ -26,6 +28,7 @@ func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
|||||||
Host: req.Host,
|
Host: req.Host,
|
||||||
Method: req.Method,
|
Method: req.Method,
|
||||||
Path: req.URL.Path,
|
Path: req.URL.Path,
|
||||||
|
Domain: r.opts.Domain,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,5 +37,5 @@ func (r *Resolver) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
||||||
return &Resolver{}
|
return &Resolver{opts: resolver.NewOptions(opts...)}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
|||||||
Host: req.Host,
|
Host: req.Host,
|
||||||
Method: req.Method,
|
Method: req.Method,
|
||||||
Path: req.URL.Path,
|
Path: req.URL.Path,
|
||||||
|
Domain: r.opts.Domain,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,23 +1,17 @@
|
|||||||
package resolver
|
package resolver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"github.com/micro/go-micro/v2/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewOptions returns new initialised options
|
type Options struct {
|
||||||
func NewOptions(opts ...Option) Options {
|
Handler string
|
||||||
var options Options
|
Domain string
|
||||||
for _, o := range opts {
|
ServicePrefix string
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.Namespace == nil {
|
|
||||||
options.Namespace = StaticNamespace("go.micro")
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Option func(o *Options)
|
||||||
|
|
||||||
// WithHandler sets the handler being used
|
// WithHandler sets the handler being used
|
||||||
func WithHandler(h string) Option {
|
func WithHandler(h string) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@ -25,9 +19,29 @@ func WithHandler(h string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithNamespace sets the function which determines the namespace for a request
|
// WithDomain sets the namespace option
|
||||||
func WithNamespace(n func(*http.Request) string) Option {
|
func WithDomain(n string) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.Namespace = n
|
o.Domain = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithServicePrefix sets the ServicePrefix option
|
||||||
|
func WithServicePrefix(p string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.ServicePrefix = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptions returns new initialised options
|
||||||
|
func NewOptions(opts ...Option) Options {
|
||||||
|
var options Options
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
if len(options.Domain) == 0 {
|
||||||
|
options.Domain = registry.DefaultDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
@ -18,13 +18,13 @@ func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(req.URL.Path[1:], "/")
|
parts := strings.Split(req.URL.Path[1:], "/")
|
||||||
ns := r.opts.Namespace(req)
|
|
||||||
|
|
||||||
return &resolver.Endpoint{
|
return &resolver.Endpoint{
|
||||||
Name: ns + "." + parts[0],
|
Name: r.opts.ServicePrefix + "." + parts[0],
|
||||||
Host: req.Host,
|
Host: req.Host,
|
||||||
Method: req.Method,
|
Method: req.Method,
|
||||||
Path: req.URL.Path,
|
Path: req.URL.Path,
|
||||||
|
Domain: r.opts.Domain,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,18 +27,6 @@ type Endpoint struct {
|
|||||||
Method string
|
Method string
|
||||||
// HTTP Path e.g /greeter.
|
// HTTP Path e.g /greeter.
|
||||||
Path string
|
Path string
|
||||||
}
|
// Domain endpoint exists within
|
||||||
|
Domain string
|
||||||
type Options struct {
|
|
||||||
Handler string
|
|
||||||
Namespace func(*http.Request) string
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
86
api/resolver/subdomain/subdomain.go
Normal file
86
api/resolver/subdomain/subdomain.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
// Package subdomain is a resolver which uses the subdomain to determine the domain to route to. It
|
||||||
|
// offloads the endpoint resolution to a child resolver which is provided in New.
|
||||||
|
package subdomain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/micro/go-micro/v2/api/resolver"
|
||||||
|
"github.com/micro/go-micro/v2/logger"
|
||||||
|
"golang.org/x/net/publicsuffix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewResolver(parent resolver.Resolver, opts ...resolver.Option) resolver.Resolver {
|
||||||
|
options := resolver.NewOptions(opts...)
|
||||||
|
return &Resolver{options, parent}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resolver struct {
|
||||||
|
opts resolver.Options
|
||||||
|
resolver.Resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
||||||
|
// resolve the endpoint using the provided resolver
|
||||||
|
endpoint, err := r.Resolver.Resolve(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// override the domain
|
||||||
|
endpoint.Domain = r.resolveDomain(req)
|
||||||
|
|
||||||
|
// return the result
|
||||||
|
return endpoint, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) resolveDomain(req *http.Request) string {
|
||||||
|
// determine the host, e.g. foobar.m3o.app
|
||||||
|
host := req.URL.Hostname()
|
||||||
|
if len(host) == 0 {
|
||||||
|
if h, _, err := net.SplitHostPort(req.Host); err == nil {
|
||||||
|
host = h // host does contain a port
|
||||||
|
} else if strings.Contains(err.Error(), "missing port in address") {
|
||||||
|
host = req.Host // host does not contain a port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for an ip address
|
||||||
|
if net.ParseIP(host) != nil {
|
||||||
|
return r.opts.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for dev enviroment
|
||||||
|
if host == "localhost" || host == "127.0.0.1" {
|
||||||
|
return r.opts.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract the top level domain plus one (e.g. 'myapp.com')
|
||||||
|
domain, err := publicsuffix.EffectiveTLDPlusOne(host)
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("Unable to extract domain from %v", host)
|
||||||
|
return r.opts.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// there was no subdomain
|
||||||
|
if host == domain {
|
||||||
|
return r.opts.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the domain from the host, leaving the subdomain, e.g. "staging.foo.myapp.com" => "staging.foo"
|
||||||
|
subdomain := strings.TrimSuffix(host, "."+domain)
|
||||||
|
|
||||||
|
// return the reversed subdomain as the namespace, e.g. "staging.foo" => "foo-staging"
|
||||||
|
comps := strings.Split(subdomain, ".")
|
||||||
|
for i := len(comps)/2 - 1; i >= 0; i-- {
|
||||||
|
opp := len(comps) - 1 - i
|
||||||
|
comps[i], comps[opp] = comps[opp], comps[i]
|
||||||
|
}
|
||||||
|
return strings.Join(comps, "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) String() string {
|
||||||
|
return "subdomain"
|
||||||
|
}
|
71
api/resolver/subdomain/subdomain_test.go
Normal file
71
api/resolver/subdomain/subdomain_test.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package subdomain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/micro/go-micro/v2/api/resolver/vpath"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolve(t *testing.T) {
|
||||||
|
tt := []struct {
|
||||||
|
Name string
|
||||||
|
Host string
|
||||||
|
Result string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "Top level domain",
|
||||||
|
Host: "micro.mu",
|
||||||
|
Result: "micro",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Effective top level domain",
|
||||||
|
Host: "micro.com.au",
|
||||||
|
Result: "micro",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Subdomain dev",
|
||||||
|
Host: "dev.micro.mu",
|
||||||
|
Result: "dev",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Subdomain foo",
|
||||||
|
Host: "foo.micro.mu",
|
||||||
|
Result: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Multi-level subdomain",
|
||||||
|
Host: "staging.myapp.m3o.app",
|
||||||
|
Result: "myapp-staging",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Dev host",
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Result: "micro",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Localhost",
|
||||||
|
Host: "localhost",
|
||||||
|
Result: "micro",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "IP host",
|
||||||
|
Host: "81.151.101.146",
|
||||||
|
Result: "micro",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tt {
|
||||||
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
r := NewResolver(vpath.NewResolver())
|
||||||
|
result, err := r.Resolve(&http.Request{URL: &url.URL{Host: tc.Host, Path: "foo/bar"}})
|
||||||
|
assert.Nil(t, err, "Expecter err to be nil")
|
||||||
|
if result != nil {
|
||||||
|
assert.Equal(t, tc.Result, result.Domain, "Expected %v but got %v", tc.Result, result.Domain)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -30,28 +30,31 @@ func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
|||||||
parts := strings.Split(req.URL.Path[1:], "/")
|
parts := strings.Split(req.URL.Path[1:], "/")
|
||||||
if len(parts) == 1 {
|
if len(parts) == 1 {
|
||||||
return &resolver.Endpoint{
|
return &resolver.Endpoint{
|
||||||
Name: r.withNamespace(req, parts...),
|
Name: r.withPrefix(parts...),
|
||||||
Host: req.Host,
|
Host: req.Host,
|
||||||
Method: req.Method,
|
Method: req.Method,
|
||||||
Path: req.URL.Path,
|
Path: req.URL.Path,
|
||||||
|
Domain: r.opts.Domain,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// /v1/foo
|
// /v1/foo
|
||||||
if re.MatchString(parts[0]) {
|
if re.MatchString(parts[0]) {
|
||||||
return &resolver.Endpoint{
|
return &resolver.Endpoint{
|
||||||
Name: r.withNamespace(req, parts[0:2]...),
|
Name: r.withPrefix(parts[0:2]...),
|
||||||
Host: req.Host,
|
Host: req.Host,
|
||||||
Method: req.Method,
|
Method: req.Method,
|
||||||
Path: req.URL.Path,
|
Path: req.URL.Path,
|
||||||
|
Domain: r.opts.Domain,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &resolver.Endpoint{
|
return &resolver.Endpoint{
|
||||||
Name: r.withNamespace(req, parts[0]),
|
Name: r.withPrefix(parts[0]),
|
||||||
Host: req.Host,
|
Host: req.Host,
|
||||||
Method: req.Method,
|
Method: req.Method,
|
||||||
Path: req.URL.Path,
|
Path: req.URL.Path,
|
||||||
|
Domain: r.opts.Domain,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,11 +62,12 @@ func (r *Resolver) String() string {
|
|||||||
return "path"
|
return "path"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) withNamespace(req *http.Request, parts ...string) string {
|
// withPrefix transforms "foo" into "go.micro.api.foo"
|
||||||
ns := r.opts.Namespace(req)
|
func (r *Resolver) withPrefix(parts ...string) string {
|
||||||
if len(ns) == 0 {
|
p := r.opts.ServicePrefix
|
||||||
return strings.Join(parts, ".")
|
if len(p) > 0 {
|
||||||
|
parts = append([]string{p}, parts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(append([]string{ns}, parts...), ".")
|
return strings.Join(parts, ".")
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@ func (r *registryRouter) process(res *registry.Result) {
|
|||||||
service, err := r.rc.GetService(res.Service.Name)
|
service, err := r.rc.GetService(res.Service.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||||
logger.Errorf("unable to get service: %v", err)
|
logger.Errorf("unable to get %v service: %v", res.Service.Name, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user