mirror of https://github.com/status-im/consul.git
Merge pull request #4392 from hashicorp/connect-sdk-http
Implement missing HTTP host to ConsulResolver func for Connect SDK.
This commit is contained in:
commit
4ec8c489c0
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/connect"
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
|
@ -202,3 +203,95 @@ func (cr *ConsulResolver) queryOptions(ctx context.Context) *api.QueryOptions {
|
||||||
}
|
}
|
||||||
return q.WithContext(ctx)
|
return q.WithContext(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConsulResolverFromAddrFunc returns a function for constructing ConsulResolver
|
||||||
|
// from a consul DNS formatted hostname (e.g. foo.service.consul or
|
||||||
|
// foo.query.consul).
|
||||||
|
//
|
||||||
|
// Note, the returned ConsulResolver resolves the query via regular agent HTTP
|
||||||
|
// discovery API. DNS is not needed or used for discovery, only the hostname
|
||||||
|
// format re-used for consistency.
|
||||||
|
func ConsulResolverFromAddrFunc(client *api.Client) func(addr string) (Resolver, error) {
|
||||||
|
// Capture client dependency
|
||||||
|
return func(addr string) (Resolver, error) {
|
||||||
|
// Http clients might provide hostname and port
|
||||||
|
host := strings.ToLower(stripPort(addr))
|
||||||
|
|
||||||
|
// For now we force use of `.consul` TLD regardless of the configured domain
|
||||||
|
// on the cluster. That's because we don't know that domain here and it
|
||||||
|
// would be really complicated to discover it inline here. We do however
|
||||||
|
// need to be able to distingush a hostname with the optional datacenter
|
||||||
|
// segment which we can't do unambiguously if we allow arbitrary trailing
|
||||||
|
// domains.
|
||||||
|
domain := ".consul"
|
||||||
|
if !strings.HasSuffix(host, domain) {
|
||||||
|
return nil, fmt.Errorf("invalid Consul DNS domain: note Connect SDK " +
|
||||||
|
"currently requires use of .consul domain even if cluster is " +
|
||||||
|
"configured with a different domain.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the domain suffix
|
||||||
|
host = host[0 : len(host)-len(domain)]
|
||||||
|
|
||||||
|
parts := strings.Split(host, ".")
|
||||||
|
numParts := len(parts)
|
||||||
|
|
||||||
|
r := &ConsulResolver{
|
||||||
|
Client: client,
|
||||||
|
Namespace: "default",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that 3 segments may be a valid DNS name like
|
||||||
|
// <tag>.<service>.service.consul but not one we support, it might also be
|
||||||
|
// <service>.service.<datacenter>.consul which we do want to support so we
|
||||||
|
// have to figure out if the last segment is supported keyword and if not
|
||||||
|
// check if the supported keyword is further up...
|
||||||
|
|
||||||
|
// To simplify logic for now, we must match one of the following (not domain
|
||||||
|
// is stripped):
|
||||||
|
// <name>.[service|query]
|
||||||
|
// <name>.[service|query].<dc>
|
||||||
|
if numParts < 2 || numParts > 3 || !supportedTypeLabel(parts[1]) {
|
||||||
|
return nil, fmt.Errorf("unsupported Consul DNS domain: must be either " +
|
||||||
|
"<name>.service[.<datacenter>].consul or " +
|
||||||
|
"<name>.query[.<datacenter>].consul")
|
||||||
|
}
|
||||||
|
|
||||||
|
if numParts == 3 {
|
||||||
|
// Must be datacenter case
|
||||||
|
r.Datacenter = parts[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// By know we must have a supported query type which means at least 2
|
||||||
|
// elements with first 2 being name, and type respectively.
|
||||||
|
r.Name = parts[0]
|
||||||
|
switch parts[1] {
|
||||||
|
case "service":
|
||||||
|
r.Type = ConsulResolverTypeService
|
||||||
|
case "query":
|
||||||
|
r.Type = ConsulResolverTypePreparedQuery
|
||||||
|
default:
|
||||||
|
// This should never happen (tm) unless the supportedTypeLabel
|
||||||
|
// implementation is changed and this switch isn't.
|
||||||
|
return nil, fmt.Errorf("invalid discovery type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func supportedTypeLabel(label string) bool {
|
||||||
|
return label == "service" || label == "query"
|
||||||
|
}
|
||||||
|
|
||||||
|
// stripPort copied from net/url/url.go
|
||||||
|
func stripPort(hostport string) string {
|
||||||
|
colon := strings.IndexByte(hostport, ':')
|
||||||
|
if colon == -1 {
|
||||||
|
return hostport
|
||||||
|
}
|
||||||
|
if i := strings.IndexByte(hostport, ']'); i != -1 {
|
||||||
|
return strings.TrimPrefix(hostport[:i], "[")
|
||||||
|
}
|
||||||
|
return hostport[:colon]
|
||||||
|
}
|
||||||
|
|
|
@ -216,3 +216,110 @@ func TestConsulResolver_Resolve(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConsulResolverFromAddrFunc(t *testing.T) {
|
||||||
|
// Don't need an actual instance since we don't do the service discovery but
|
||||||
|
// we do want to assert the client is pass through correctly.
|
||||||
|
client, err := api.NewClient(api.DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
addr string
|
||||||
|
want Resolver
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "service",
|
||||||
|
addr: "foo.service.consul",
|
||||||
|
want: &ConsulResolver{
|
||||||
|
Client: client,
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "foo",
|
||||||
|
Type: ConsulResolverTypeService,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query",
|
||||||
|
addr: "foo.query.consul",
|
||||||
|
want: &ConsulResolver{
|
||||||
|
Client: client,
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "foo",
|
||||||
|
Type: ConsulResolverTypePreparedQuery,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service with dc",
|
||||||
|
addr: "foo.service.dc2.consul",
|
||||||
|
want: &ConsulResolver{
|
||||||
|
Client: client,
|
||||||
|
Datacenter: "dc2",
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "foo",
|
||||||
|
Type: ConsulResolverTypeService,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query with dc",
|
||||||
|
addr: "foo.query.dc2.consul",
|
||||||
|
want: &ConsulResolver{
|
||||||
|
Client: client,
|
||||||
|
Datacenter: "dc2",
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "foo",
|
||||||
|
Type: ConsulResolverTypePreparedQuery,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid host:port",
|
||||||
|
addr: "%%%",
|
||||||
|
wantErr: "invalid Consul DNS domain",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom domain",
|
||||||
|
addr: "foo.service.my-consul.com",
|
||||||
|
wantErr: "invalid Consul DNS domain",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported query type",
|
||||||
|
addr: "foo.connect.consul",
|
||||||
|
wantErr: "unsupported Consul DNS domain",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported query type and datacenter",
|
||||||
|
addr: "foo.connect.dc1.consul",
|
||||||
|
wantErr: "unsupported Consul DNS domain",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported query type and datacenter",
|
||||||
|
addr: "foo.connect.dc1.consul",
|
||||||
|
wantErr: "unsupported Consul DNS domain",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported tag filter",
|
||||||
|
addr: "tag1.foo.service.consul",
|
||||||
|
wantErr: "unsupported Consul DNS domain",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported tag filter with DC",
|
||||||
|
addr: "tag1.foo.service.dc1.consul",
|
||||||
|
wantErr: "unsupported Consul DNS domain",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
fn := ConsulResolverFromAddrFunc(client)
|
||||||
|
got, gotErr := fn(tt.addr)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.Error(gotErr)
|
||||||
|
require.Contains(gotErr.Error(), tt.wantErr)
|
||||||
|
} else {
|
||||||
|
require.NoError(gotErr)
|
||||||
|
require.Equal(tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -69,10 +69,11 @@ func NewService(serviceName string, client *api.Client) (*Service, error) {
|
||||||
func NewServiceWithLogger(serviceName string, client *api.Client,
|
func NewServiceWithLogger(serviceName string, client *api.Client,
|
||||||
logger *log.Logger) (*Service, error) {
|
logger *log.Logger) (*Service, error) {
|
||||||
s := &Service{
|
s := &Service{
|
||||||
service: serviceName,
|
service: serviceName,
|
||||||
client: client,
|
client: client,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
tlsCfg: newDynamicTLSConfig(defaultTLSConfig()),
|
tlsCfg: newDynamicTLSConfig(defaultTLSConfig()),
|
||||||
|
httpResolverFromAddr: ConsulResolverFromAddrFunc(client),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up root and leaf watches
|
// Set up root and leaf watches
|
||||||
|
|
|
@ -252,3 +252,28 @@ func TestService_HTTPClient(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestService_HasDefaultHTTPResolverFromAddr(t *testing.T) {
|
||||||
|
|
||||||
|
client, err := api.NewClient(api.DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s, err := NewService("foo", client)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Sanity check this is actually set in constructor since we always override
|
||||||
|
// it in tests. Full tests of the resolver func are in resolver_test.go
|
||||||
|
require.NotNil(t, s.httpResolverFromAddr)
|
||||||
|
|
||||||
|
fn := s.httpResolverFromAddr
|
||||||
|
|
||||||
|
expected := &ConsulResolver{
|
||||||
|
Client: client,
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "foo",
|
||||||
|
Type: ConsulResolverTypeService,
|
||||||
|
}
|
||||||
|
got, err := fn("foo.service.consul")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expected, got)
|
||||||
|
}
|
||||||
|
|
|
@ -157,14 +157,33 @@ The HTTP client configuration automatically sends the correct client
|
||||||
certificate, verifies the server certificate, and manages background
|
certificate, verifies the server certificate, and manages background
|
||||||
goroutines for updating our certificates as necessary.
|
goroutines for updating our certificates as necessary.
|
||||||
|
|
||||||
-> **Important:** The HTTP client _requires_ the hostname is a Consul
|
|
||||||
DNS name. Static IP addresses and external DNS cannot be used with the
|
|
||||||
HTTP client. For these values, please use `svc.Dial` directly.
|
|
||||||
|
|
||||||
If the application already uses a manually constructed `*http.Client`,
|
If the application already uses a manually constructed `*http.Client`,
|
||||||
the `svc.HTTPDialTLS` function can be used to configure the
|
the `svc.HTTPDialTLS` function can be used to configure the
|
||||||
`http.Transport.DialTLS` field to achieve equivalent behavior.
|
`http.Transport.DialTLS` field to achieve equivalent behavior.
|
||||||
|
|
||||||
|
### Hostname Requirements
|
||||||
|
|
||||||
|
The hostname used in the request URL is used to identify the logical service
|
||||||
|
discovery mechanism for the target. **It's not actually resolved via DNS** but
|
||||||
|
used as a logical identifier for a Consul service discovery mechanism. It has
|
||||||
|
the following specific limitations:
|
||||||
|
|
||||||
|
* The scheme must be `https://`.
|
||||||
|
* It must be a Consul DNS name in one of the following forms:
|
||||||
|
* `<name>.service[.<datacenter>].consul` to discover a healthy service
|
||||||
|
instance for a given service.
|
||||||
|
* `<name>.query[.<datacenter>].consul` to discover an instance via
|
||||||
|
[Prepared Query](/api/query.html).
|
||||||
|
* The top-level domain _must_ be `.consul` even if your cluster has a custom
|
||||||
|
`domain` configured for it's DNS interface. This might be relaxed in the
|
||||||
|
future.
|
||||||
|
* Tag filters for services are not currently supported (i.e.
|
||||||
|
`tag1.web.service.consul`) however the same behaviour can be achieved using a
|
||||||
|
prepared query.
|
||||||
|
* External DNS names, raw IP addresses and so on will cause an error and should
|
||||||
|
be fetched using a separate `HTTPClient`.
|
||||||
|
|
||||||
|
|
||||||
## Raw TLS Connection
|
## Raw TLS Connection
|
||||||
|
|
||||||
For a raw `net.Conn` TLS connection, the `svc.Dial` function can be used.
|
For a raw `net.Conn` TLS connection, the `svc.Dial` function can be used.
|
||||||
|
|
Loading…
Reference in New Issue