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:
ben-toogood 2020-06-26 14:28:18 +01:00 committed by GitHub
parent 4f0f4326df
commit 104b7d8f8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 211 additions and 44 deletions

View File

@ -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),
)), )),
) )

View File

@ -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...)}
} }

View File

@ -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
} }

View File

@ -1,22 +1,16 @@
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 { type Option func(o *Options)
options.Namespace = StaticNamespace("go.micro")
}
return options
}
// WithHandler sets the handler being used // WithHandler sets the handler being used
func WithHandler(h string) Option { func WithHandler(h string) Option {
@ -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
}

View File

@ -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
} }

View File

@ -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
}
} }

View 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"
}

View 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)
}
})
}
}

View File

@ -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, ".")
} }

View File

@ -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
} }