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.WithRegistry(r),
|
||||
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"
|
||||
)
|
||||
|
||||
type Resolver struct{}
|
||||
type Resolver struct {
|
||||
opts resolver.Options
|
||||
}
|
||||
|
||||
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
||||
// /foo.Bar/Service
|
||||
@ -26,6 +28,7 @@ func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
Domain: r.opts.Domain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -34,5 +37,5 @@ func (r *Resolver) String() string {
|
||||
}
|
||||
|
||||
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,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
Domain: r.opts.Domain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -1,23 +1,17 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/micro/go-micro/v2/registry"
|
||||
)
|
||||
|
||||
// NewOptions returns new initialised options
|
||||
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
|
||||
type Options struct {
|
||||
Handler string
|
||||
Domain string
|
||||
ServicePrefix string
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
// WithHandler sets the handler being used
|
||||
func WithHandler(h string) Option {
|
||||
return func(o *Options) {
|
||||
@ -25,9 +19,29 @@ func WithHandler(h string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithNamespace sets the function which determines the namespace for a request
|
||||
func WithNamespace(n func(*http.Request) string) Option {
|
||||
// WithDomain sets the namespace option
|
||||
func WithDomain(n string) Option {
|
||||
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:], "/")
|
||||
ns := r.opts.Namespace(req)
|
||||
|
||||
return &resolver.Endpoint{
|
||||
Name: ns + "." + parts[0],
|
||||
Name: r.opts.ServicePrefix + "." + parts[0],
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
Domain: r.opts.Domain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -27,18 +27,6 @@ type Endpoint struct {
|
||||
Method string
|
||||
// HTTP Path e.g /greeter.
|
||||
Path 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
|
||||
}
|
||||
// Domain endpoint exists within
|
||||
Domain string
|
||||
}
|
||||
|
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:], "/")
|
||||
if len(parts) == 1 {
|
||||
return &resolver.Endpoint{
|
||||
Name: r.withNamespace(req, parts...),
|
||||
Name: r.withPrefix(parts...),
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
Domain: r.opts.Domain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// /v1/foo
|
||||
if re.MatchString(parts[0]) {
|
||||
return &resolver.Endpoint{
|
||||
Name: r.withNamespace(req, parts[0:2]...),
|
||||
Name: r.withPrefix(parts[0:2]...),
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
Domain: r.opts.Domain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &resolver.Endpoint{
|
||||
Name: r.withNamespace(req, parts[0]),
|
||||
Name: r.withPrefix(parts[0]),
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
Domain: r.opts.Domain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -59,11 +62,12 @@ 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, ".")
|
||||
// withPrefix transforms "foo" into "go.micro.api.foo"
|
||||
func (r *Resolver) withPrefix(parts ...string) string {
|
||||
p := r.opts.ServicePrefix
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user