diff --git a/api/handler/http/http_test.go b/api/handler/http/http_test.go index f0ccd1ff..1cf98d9f 100644 --- a/api/handler/http/http_test.go +++ b/api/handler/http/http_test.go @@ -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), )), ) diff --git a/api/resolver/grpc/grpc.go b/api/resolver/grpc/grpc.go index 97ea279f..3cfe7871 100644 --- a/api/resolver/grpc/grpc.go +++ b/api/resolver/grpc/grpc.go @@ -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...)} } diff --git a/api/resolver/host/host.go b/api/resolver/host/host.go index 204bdfa8..f97e9415 100644 --- a/api/resolver/host/host.go +++ b/api/resolver/host/host.go @@ -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 } diff --git a/api/resolver/options.go b/api/resolver/options.go index a6fa0ab7..1c48313e 100644 --- a/api/resolver/options.go +++ b/api/resolver/options.go @@ -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 +} diff --git a/api/resolver/path/path.go b/api/resolver/path/path.go index 84a92656..dbce6e09 100644 --- a/api/resolver/path/path.go +++ b/api/resolver/path/path.go @@ -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 } diff --git a/api/resolver/resolver.go b/api/resolver/resolver.go index 81e8194d..110995d8 100644 --- a/api/resolver/resolver.go +++ b/api/resolver/resolver.go @@ -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 } diff --git a/api/resolver/subdomain/subdomain.go b/api/resolver/subdomain/subdomain.go new file mode 100644 index 00000000..199fcde4 --- /dev/null +++ b/api/resolver/subdomain/subdomain.go @@ -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" +} diff --git a/api/resolver/subdomain/subdomain_test.go b/api/resolver/subdomain/subdomain_test.go new file mode 100644 index 00000000..dbbbfa82 --- /dev/null +++ b/api/resolver/subdomain/subdomain_test.go @@ -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) + } + }) + } +} diff --git a/api/resolver/vpath/vpath.go b/api/resolver/vpath/vpath.go index f653a86f..99486074 100644 --- a/api/resolver/vpath/vpath.go +++ b/api/resolver/vpath/vpath.go @@ -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, ".") } diff --git a/api/router/registry/registry.go b/api/router/registry/registry.go index cdd3de22..6fd92c16 100644 --- a/api/router/registry/registry.go +++ b/api/router/registry/registry.go @@ -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 }