package connect
import (
"context"
"fmt"
"math/rand"
"strings"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/ipaddr"
)
// Resolver is the interface implemented by a service discovery mechanism to get
// the address and identity of an instance to connect to via Connect as a
// client.
type Resolver interface {
// Resolve returns a single service instance to connect to. Implementations
// may attempt to ensure the instance returned is currently available. It is
// expected that a client will re-dial on a connection failure so making an
// effort to return a different service instance each time where available
// increases reliability. The context passed can be used to impose timeouts
// which may or may not be respected by implementations that make network
// calls to resolve the service. The addr returned is a string in any valid
// form for passing directly to `net.Dial("tcp", addr)`. The certURI
// represents the identity of the service instance. It will be matched against
// the TLS certificate URI SAN presented by the server and the connection
// rejected if they don't match.
Resolve ( ctx context . Context ) ( addr string , certURI connect . CertURI , err error )
}
// StaticResolver is a statically defined resolver. This can be used to Dial a
// known Connect endpoint without performing service discovery.
type StaticResolver struct {
// Addr is the network address (including port) of the instance. It must be
// the connect-enabled mTLS listener and may be a proxy in front of the actual
// target service process. It is a string in any valid form for passing
// directly to net.Dial("tcp", addr).
Addr string
// CertURL is the identity we expect the server to present in it's TLS
// certificate. It must be an exact URI string match or the connection will be
// rejected.
CertURI connect . CertURI
}
// Resolve implements Resolver by returning the static values.
func ( sr * StaticResolver ) Resolve ( ctx context . Context ) ( string , connect . CertURI , error ) {
return sr . Addr , sr . CertURI , nil
}
const (
// ConsulResolverTypeService indicates resolving healthy service nodes.
ConsulResolverTypeService int = iota
// ConsulResolverTypePreparedQuery indicates resolving via prepared query.
ConsulResolverTypePreparedQuery
)
// ConsulResolver queries Consul for a service instance.
type ConsulResolver struct {
// Client is the Consul API client to use. Must be non-nil or Resolve will
// panic.
Client * api . Client
// Namespace of the query target.
Namespace string
// Name of the query target.
Name string
// Type of the query target. Should be one of the defined ConsulResolverType*
// constants. Currently defaults to ConsulResolverTypeService.
Type int
// Datacenter to resolve in, empty indicates agent's local DC.
Datacenter string
}
// Resolve performs service discovery against the local Consul agent and returns
// the address and expected identity of a suitable service instance.
func ( cr * ConsulResolver ) Resolve ( ctx context . Context ) ( string , connect . CertURI , error ) {
switch cr . Type {
case ConsulResolverTypeService :
return cr . resolveService ( ctx )
case ConsulResolverTypePreparedQuery :
return cr . resolveQuery ( ctx )
default :
return "" , nil , fmt . Errorf ( "unknown resolver type" )
}
}
func ( cr * ConsulResolver ) resolveService ( ctx context . Context ) ( string , connect . CertURI , error ) {
health := cr . Client . Health ( )
svcs , _ , err := health . Connect ( cr . Name , "" , true , cr . queryOptions ( ctx ) )
if err != nil {
return "" , nil , err
}
if len ( svcs ) < 1 {
return "" , nil , fmt . Errorf ( "no healthy instances found" )
}
// Services are not shuffled by HTTP API, pick one at (pseudo) random.
idx := 0
if len ( svcs ) > 1 {
idx = rand . Intn ( len ( svcs ) )
}
return cr . resolveServiceEntry ( svcs [ idx ] )
}
func ( cr * ConsulResolver ) resolveQuery ( ctx context . Context ) ( string , connect . CertURI , error ) {
resp , _ , err := cr . Client . PreparedQuery ( ) . Execute ( cr . Name , cr . queryOptions ( ctx ) )
if err != nil {
return "" , nil , err
}
svcs := resp . Nodes
if len ( svcs ) < 1 {
return "" , nil , fmt . Errorf ( "no healthy instances found" )
}
// Services are not shuffled by HTTP API, pick one at (pseudo) random.
idx := 0
if len ( svcs ) > 1 {
idx = rand . Intn ( len ( svcs ) )
}
return cr . resolveServiceEntry ( & svcs [ idx ] )
}
func ( cr * ConsulResolver ) resolveServiceEntry ( entry * api . ServiceEntry ) ( string , connect . CertURI , error ) {
addr := entry . Service . Address
if addr == "" {
addr = entry . Node . Address
}
port := entry . Service . Port
service := entry . Service . Proxy . DestinationServiceName
if entry . Service . Connect != nil && entry . Service . Connect . Native {
service = entry . Service . Service
}
if service == "" {
// Shouldn't happen but to protect against bugs in agent API returning bad
// service response...
return "" , nil , fmt . Errorf ( "not a valid connect service" )
}
// Generate the expected CertURI
certURI := & connect . SpiffeIDService {
// No host since we don't validate trust domain here (we rely on x509 to
// prove trust).
Namespace : "default" ,
Datacenter : entry . Node . Datacenter ,
Service : service ,
}
return ipaddr . FormatAddressPort ( addr , port ) , certURI , nil
}
func ( cr * ConsulResolver ) queryOptions ( ctx context . Context ) * api . QueryOptions {
q := & api . QueryOptions {
// We may make this configurable one day but we may also implement our own
// caching which is even more stale so...
AllowStale : true ,
Datacenter : cr . Datacenter ,
// For prepared queries
Connect : true ,
}
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 distinguish 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 ]
}