2019-06-27 17:37:43 +00:00
package structs
import (
2019-08-19 20:31:05 +00:00
"encoding/json"
2019-06-27 17:37:43 +00:00
"fmt"
"math"
2019-07-24 01:50:23 +00:00
"regexp"
2019-06-27 17:37:43 +00:00
"sort"
2019-07-02 03:10:51 +00:00
"strconv"
2019-07-24 01:50:23 +00:00
"strings"
2019-06-27 17:37:43 +00:00
"time"
"github.com/hashicorp/consul/acl"
2019-07-02 03:10:51 +00:00
"github.com/hashicorp/consul/agent/cache"
2019-12-06 16:14:56 +00:00
"github.com/hashicorp/consul/lib"
2019-07-02 03:10:51 +00:00
"github.com/mitchellh/hashstructure"
2019-06-27 17:37:43 +00:00
)
// ServiceRouterConfigEntry defines L7 (e.g. http) routing rules for a named
// service exposed in Connect.
//
// This config entry represents the topmost part of the discovery chain. Only
// one router config will be used per resolved discovery chain and is not
// otherwise discovered recursively (unlike splitter and resolver config
// entries).
//
// Router config entries will be restricted to only services that define their
// protocol as http-based (in centralized configuration).
type ServiceRouterConfigEntry struct {
Kind string
Name string
// Routes is the list of routes to consider when processing L7 requests.
// The first rule to match in the list is terminal and stops further
// evaluation.
//
// Traffic that fails to match any of the provided routes will be routed to
// the default service.
Routes [ ] ServiceRoute
2020-01-24 15:04:58 +00:00
EnterpriseMeta ` hcl:",squash" mapstructure:",squash" `
2019-06-27 17:37:43 +00:00
RaftIndex
}
func ( e * ServiceRouterConfigEntry ) GetKind ( ) string {
return ServiceRouter
}
func ( e * ServiceRouterConfigEntry ) GetName ( ) string {
if e == nil {
return ""
}
return e . Name
}
func ( e * ServiceRouterConfigEntry ) Normalize ( ) error {
if e == nil {
return fmt . Errorf ( "config entry is nil" )
}
e . Kind = ServiceRouter
2020-01-24 15:04:58 +00:00
e . EnterpriseMeta . Normalize ( )
2019-07-24 01:56:39 +00:00
for _ , route := range e . Routes {
if route . Match == nil || route . Match . HTTP == nil {
continue
}
httpMatch := route . Match . HTTP
if len ( httpMatch . Methods ) == 0 {
continue
}
for j := 0 ; j < len ( httpMatch . Methods ) ; j ++ {
httpMatch . Methods [ j ] = strings . ToUpper ( httpMatch . Methods [ j ] )
}
2020-01-24 15:04:58 +00:00
if route . Destination != nil && route . Destination . Namespace == "" {
route . Destination . Namespace = e . EnterpriseMeta . NamespaceOrDefault ( )
}
2019-07-24 01:56:39 +00:00
}
2019-06-27 17:37:43 +00:00
return nil
}
func ( e * ServiceRouterConfigEntry ) Validate ( ) error {
if e . Name == "" {
return fmt . Errorf ( "Name is required" )
}
// Technically you can have no explicit routes at all where just the
// catch-all is configured for you, but at that point maybe you should just
// delete it so it will default?
2019-07-01 20:23:36 +00:00
for i , route := range e . Routes {
2019-07-24 01:50:23 +00:00
eligibleForPrefixRewrite := false
if route . Match != nil && route . Match . HTTP != nil {
pathParts := 0
if route . Match . HTTP . PathExact != "" {
eligibleForPrefixRewrite = true
pathParts ++
if ! strings . HasPrefix ( route . Match . HTTP . PathExact , "/" ) {
return fmt . Errorf ( "Route[%d] PathExact doesn't start with '/': %q" , i , route . Match . HTTP . PathExact )
}
2019-07-01 20:23:36 +00:00
}
2019-07-24 01:50:23 +00:00
if route . Match . HTTP . PathPrefix != "" {
eligibleForPrefixRewrite = true
pathParts ++
if ! strings . HasPrefix ( route . Match . HTTP . PathPrefix , "/" ) {
return fmt . Errorf ( "Route[%d] PathPrefix doesn't start with '/': %q" , i , route . Match . HTTP . PathPrefix )
}
2019-07-01 20:23:36 +00:00
}
2019-07-24 01:50:23 +00:00
if route . Match . HTTP . PathRegex != "" {
pathParts ++
2019-07-01 20:23:36 +00:00
}
2019-07-24 01:50:23 +00:00
if pathParts > 1 {
return fmt . Errorf ( "Route[%d] should only contain at most one of PathExact, PathPrefix, or PathRegex" , i )
2019-07-01 20:23:36 +00:00
}
2019-07-24 01:50:23 +00:00
for j , hdr := range route . Match . HTTP . Header {
if hdr . Name == "" {
return fmt . Errorf ( "Route[%d] Header[%d] missing required Name field" , i , j )
}
hdrParts := 0
if hdr . Present {
hdrParts ++
}
if hdr . Exact != "" {
hdrParts ++
}
if hdr . Regex != "" {
hdrParts ++
}
if hdr . Prefix != "" {
hdrParts ++
}
if hdr . Suffix != "" {
hdrParts ++
}
if hdrParts != 1 {
return fmt . Errorf ( "Route[%d] Header[%d] should only contain one of Present, Exact, Prefix, Suffix, or Regex" , i , j )
}
2019-07-01 20:23:36 +00:00
}
2019-07-24 01:50:23 +00:00
for j , qm := range route . Match . HTTP . QueryParam {
if qm . Name == "" {
return fmt . Errorf ( "Route[%d] QueryParam[%d] missing required Name field" , i , j )
}
2019-07-24 01:55:26 +00:00
qmParts := 0
if qm . Present {
qmParts ++
}
if qm . Exact != "" {
qmParts ++
}
if qm . Regex != "" {
qmParts ++
}
if qmParts != 1 {
return fmt . Errorf ( "Route[%d] QueryParam[%d] should only contain one of Present, Exact, or Regex" , i , j )
}
2019-07-24 01:50:23 +00:00
}
2019-07-24 01:56:39 +00:00
if len ( route . Match . HTTP . Methods ) > 0 {
found := make ( map [ string ] struct { } )
for _ , m := range route . Match . HTTP . Methods {
if _ , ok := found [ m ] ; ok {
return fmt . Errorf ( "Route[%d] Methods contains %q more than once" , i , m )
}
found [ m ] = struct { } { }
}
}
2019-07-01 20:23:36 +00:00
}
if route . Destination != nil {
2019-07-24 01:50:23 +00:00
if route . Destination . PrefixRewrite != "" && ! eligibleForPrefixRewrite {
2019-07-01 20:23:36 +00:00
return fmt . Errorf ( "Route[%d] cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix" , i )
}
}
}
2019-06-27 17:37:43 +00:00
return nil
}
func ( e * ServiceRouterConfigEntry ) CanRead ( rule acl . Authorizer ) bool {
return canReadDiscoveryChain ( e , rule )
}
func ( e * ServiceRouterConfigEntry ) CanWrite ( rule acl . Authorizer ) bool {
return canWriteDiscoveryChain ( e , rule )
}
func ( e * ServiceRouterConfigEntry ) GetRaftIndex ( ) * RaftIndex {
if e == nil {
return & RaftIndex { }
}
return & e . RaftIndex
}
2020-01-24 15:04:58 +00:00
func ( e * ServiceRouterConfigEntry ) ListRelatedServices ( ) [ ] ServiceID {
found := make ( map [ ServiceID ] struct { } )
2019-06-27 17:37:43 +00:00
// We always inject a default catch-all route to the same service as the router.
2020-02-06 15:52:25 +00:00
svcID := NewServiceID ( e . Name , & e . EnterpriseMeta )
found [ svcID ] = struct { } { }
2019-06-27 17:37:43 +00:00
for _ , route := range e . Routes {
2020-02-06 15:52:25 +00:00
if route . Destination != nil {
destID := NewServiceID ( defaultIfEmpty ( route . Destination . Service , e . Name ) , route . Destination . GetEnterpriseMeta ( & e . EnterpriseMeta ) )
if destID != svcID {
found [ destID ] = struct { } { }
}
2019-06-27 17:37:43 +00:00
}
}
2019-07-24 01:50:23 +00:00
if len ( found ) == 0 {
return nil
}
2020-01-24 15:04:58 +00:00
out := make ( [ ] ServiceID , 0 , len ( found ) )
2019-06-27 17:37:43 +00:00
for svc , _ := range found {
out = append ( out , svc )
}
2020-01-24 15:04:58 +00:00
sort . Slice ( out , func ( i , j int ) bool {
return out [ i ] . EnterpriseMeta . LessThan ( & out [ j ] . EnterpriseMeta ) ||
out [ i ] . ID < out [ j ] . ID
} )
2019-06-27 17:37:43 +00:00
return out
}
2020-01-24 15:04:58 +00:00
func ( e * ServiceRouterConfigEntry ) GetEnterpriseMeta ( ) * EnterpriseMeta {
if e == nil {
return nil
}
return & e . EnterpriseMeta
}
2019-06-27 17:37:43 +00:00
// ServiceRoute is a single routing rule that routes traffic to the destination
// when the match criteria applies.
type ServiceRoute struct {
Match * ServiceRouteMatch ` json:",omitempty" `
Destination * ServiceRouteDestination ` json:",omitempty" `
}
// ServiceRouteMatch is a set of criteria that can match incoming L7 requests.
type ServiceRouteMatch struct {
HTTP * ServiceRouteHTTPMatch ` json:",omitempty" `
// If we have non-http match criteria for other protocols in the future
// (gRPC, redis, etc) they can go here.
}
2019-07-01 20:23:36 +00:00
func ( m * ServiceRouteMatch ) IsEmpty ( ) bool {
return m . HTTP == nil || m . HTTP . IsEmpty ( )
}
2019-06-27 17:37:43 +00:00
// ServiceRouteHTTPMatch is a set of http-specific match criteria.
type ServiceRouteHTTPMatch struct {
2020-05-27 18:28:28 +00:00
PathExact string ` json:",omitempty" alias:"path_exact" `
PathPrefix string ` json:",omitempty" alias:"path_prefix" `
PathRegex string ` json:",omitempty" alias:"path_regex" `
2019-06-27 17:37:43 +00:00
Header [ ] ServiceRouteHTTPMatchHeader ` json:",omitempty" `
2020-05-27 18:28:28 +00:00
QueryParam [ ] ServiceRouteHTTPMatchQueryParam ` json:",omitempty" alias:"query_param" `
2019-07-24 01:56:39 +00:00
Methods [ ] string ` json:",omitempty" `
2019-07-01 20:23:36 +00:00
}
func ( m * ServiceRouteHTTPMatch ) IsEmpty ( ) bool {
return m . PathExact == "" &&
m . PathPrefix == "" &&
m . PathRegex == "" &&
len ( m . Header ) == 0 &&
2019-07-24 01:56:39 +00:00
len ( m . QueryParam ) == 0 &&
len ( m . Methods ) == 0
2019-06-27 17:37:43 +00:00
}
type ServiceRouteHTTPMatchHeader struct {
Name string
Present bool ` json:",omitempty" `
Exact string ` json:",omitempty" `
Prefix string ` json:",omitempty" `
Suffix string ` json:",omitempty" `
Regex string ` json:",omitempty" `
Invert bool ` json:",omitempty" `
}
type ServiceRouteHTTPMatchQueryParam struct {
2019-07-24 01:55:26 +00:00
Name string
Present bool ` json:",omitempty" `
Exact string ` json:",omitempty" `
Regex string ` json:",omitempty" `
2019-06-27 17:37:43 +00:00
}
// ServiceRouteDestination describes how to proxy the actual matching request
// to a service.
type ServiceRouteDestination struct {
// Service is the service to resolve instead of the default service. If
// empty then the default discovery chain service name is used.
Service string ` json:",omitempty" `
// ServiceSubset is a named subset of the given service to resolve instead
// of one defined as that service's DefaultSubset. If empty the default
// subset is used.
//
// If this field is specified then this route is ineligible for further
// splitting.
2020-05-27 18:28:28 +00:00
ServiceSubset string ` json:",omitempty" alias:"service_subset" `
2019-06-27 17:37:43 +00:00
// Namespace is the namespace to resolve the service from instead of the
// current namespace. If empty the current namespace is assumed.
//
// If this field is specified then this route is ineligible for further
// splitting.
Namespace string ` json:",omitempty" `
// PrefixRewrite allows for the proxied request to have its matching path
// prefix modified before being sent to the destination. Described more
// below in the envoy implementation section.
2020-05-27 18:28:28 +00:00
PrefixRewrite string ` json:",omitempty" alias:"prefix_rewrite" `
2019-06-27 17:37:43 +00:00
// RequestTimeout is the total amount of time permitted for the entire
// downstream request (and retries) to be processed.
2020-05-27 18:28:28 +00:00
RequestTimeout time . Duration ` json:",omitempty" alias:"request_timeout" `
2019-06-27 17:37:43 +00:00
// NumRetries is the number of times to retry the request when a retryable
// result occurs. This seems fairly proxy agnostic.
2020-05-27 18:28:28 +00:00
NumRetries uint32 ` json:",omitempty" alias:"num_retries" `
2019-06-27 17:37:43 +00:00
// RetryOnConnectFailure allows for connection failure errors to trigger a
// retry. This should be expressible in other proxies as it's just a layer
// 4 failure bubbling up to layer 7.
2020-05-27 18:28:28 +00:00
RetryOnConnectFailure bool ` json:",omitempty" alias:"retry_on_connect_failure" `
2019-06-27 17:37:43 +00:00
// RetryOnStatusCodes is a flat list of http response status codes that are
// eligible for retry. This again should be feasible in any sane proxy.
2020-05-27 18:28:28 +00:00
RetryOnStatusCodes [ ] uint32 ` json:",omitempty" alias:"retry_on_status_codes" `
2019-06-27 17:37:43 +00:00
}
2019-08-19 20:31:05 +00:00
func ( e * ServiceRouteDestination ) MarshalJSON ( ) ( [ ] byte , error ) {
type Alias ServiceRouteDestination
exported := & struct {
RequestTimeout string ` json:",omitempty" `
* Alias
} {
RequestTimeout : e . RequestTimeout . String ( ) ,
Alias : ( * Alias ) ( e ) ,
}
if e . RequestTimeout == 0 {
exported . RequestTimeout = ""
}
return json . Marshal ( exported )
}
func ( e * ServiceRouteDestination ) UnmarshalJSON ( data [ ] byte ) error {
type Alias ServiceRouteDestination
aux := & struct {
RequestTimeout string
* Alias
} {
Alias : ( * Alias ) ( e ) ,
}
2019-12-06 16:14:56 +00:00
if err := lib . UnmarshalJSON ( data , & aux ) ; err != nil {
2019-08-19 20:31:05 +00:00
return err
}
var err error
if aux . RequestTimeout != "" {
if e . RequestTimeout , err = time . ParseDuration ( aux . RequestTimeout ) ; err != nil {
return err
}
}
return nil
}
2019-07-12 19:16:21 +00:00
func ( d * ServiceRouteDestination ) HasRetryFeatures ( ) bool {
return d . NumRetries > 0 || d . RetryOnConnectFailure || len ( d . RetryOnStatusCodes ) > 0
}
2019-06-27 17:37:43 +00:00
// ServiceSplitterConfigEntry defines how incoming requests are split across
// different subsets of a single service (like during staged canary rollouts),
// or perhaps across different services (like during a v2 rewrite or other type
// of codebase migration).
//
// This config entry represents the next hop of the discovery chain after
// routing. If no splitter config is defined the chain assumes 100% of traffic
// goes to the default service and discovery continues on to the resolution
// hop.
//
// Splitter configs are recursively collected while walking the discovery
// chain.
//
// Splitter config entries will be restricted to only services that define
// their protocol as http-based (in centralized configuration).
type ServiceSplitterConfigEntry struct {
Kind string
Name string
// Splits is the configurations for the details of the traffic splitting.
//
// The sum of weights across all splits must add up to 100.
//
// If the split is within epsilon of 100 then the remainder is attributed
// to the FIRST split.
Splits [ ] ServiceSplit
2020-01-24 15:04:58 +00:00
EnterpriseMeta ` hcl:",squash" mapstructure:",squash" `
2019-06-27 17:37:43 +00:00
RaftIndex
}
func ( e * ServiceSplitterConfigEntry ) GetKind ( ) string {
return ServiceSplitter
}
func ( e * ServiceSplitterConfigEntry ) GetName ( ) string {
if e == nil {
return ""
}
return e . Name
}
func ( e * ServiceSplitterConfigEntry ) Normalize ( ) error {
if e == nil {
return fmt . Errorf ( "config entry is nil" )
}
e . Kind = ServiceSplitter
// This slightly massages inputs to enforce that the smallest representable
// weight is 1/10000 or .01%
2020-01-24 15:04:58 +00:00
e . EnterpriseMeta . Normalize ( )
2019-06-27 17:37:43 +00:00
if len ( e . Splits ) > 0 {
for i , split := range e . Splits {
2020-01-24 15:04:58 +00:00
if split . Namespace == "" {
split . Namespace = e . EnterpriseMeta . NamespaceOrDefault ( )
}
2019-06-27 18:38:21 +00:00
e . Splits [ i ] . Weight = NormalizeServiceSplitWeight ( split . Weight )
2019-06-27 17:37:43 +00:00
}
}
return nil
}
2019-06-27 18:38:21 +00:00
func NormalizeServiceSplitWeight ( weight float32 ) float32 {
weightScaled := scaleWeight ( weight )
2020-04-16 17:35:28 +00:00
return float32 ( weightScaled ) / 100.0
2019-06-27 18:38:21 +00:00
}
2019-06-27 17:37:43 +00:00
func ( e * ServiceSplitterConfigEntry ) Validate ( ) error {
if e . Name == "" {
return fmt . Errorf ( "Name is required" )
}
if len ( e . Splits ) == 0 {
return fmt . Errorf ( "no splits configured" )
}
const maxScaledWeight = 100 * 100
copyAsKey := func ( s ServiceSplit ) ServiceSplit {
s . Weight = 0
return s
}
// Make sure we didn't refer to the same thing twice.
found := make ( map [ ServiceSplit ] struct { } )
for _ , split := range e . Splits {
splitKey := copyAsKey ( split )
if splitKey . Service == "" {
splitKey . Service = e . Name
}
if _ , ok := found [ splitKey ] ; ok {
return fmt . Errorf (
"split destination occurs more than once: service=%q, subset=%q, namespace=%q" ,
splitKey . Service , splitKey . ServiceSubset , splitKey . Namespace ,
)
}
found [ splitKey ] = struct { } { }
}
sumScaled := 0
for _ , split := range e . Splits {
sumScaled += scaleWeight ( split . Weight )
}
if sumScaled != maxScaledWeight {
return fmt . Errorf ( "the sum of all split weights must be 100, not %f" , float32 ( sumScaled ) / 100 )
}
return nil
}
// scaleWeight assumes the input is a value between 0 and 100 representing
// shares out of a percentile range. The function will convert to a unit
// representing 0.01% units in the same manner as you may convert $0.98 to 98
// cents.
func scaleWeight ( v float32 ) int {
return int ( math . Round ( float64 ( v * 100.0 ) ) )
}
func ( e * ServiceSplitterConfigEntry ) CanRead ( rule acl . Authorizer ) bool {
return canReadDiscoveryChain ( e , rule )
}
func ( e * ServiceSplitterConfigEntry ) CanWrite ( rule acl . Authorizer ) bool {
return canWriteDiscoveryChain ( e , rule )
}
func ( e * ServiceSplitterConfigEntry ) GetRaftIndex ( ) * RaftIndex {
if e == nil {
return & RaftIndex { }
}
return & e . RaftIndex
}
2020-01-24 15:04:58 +00:00
func ( e * ServiceSplitterConfigEntry ) GetEnterpriseMeta ( ) * EnterpriseMeta {
if e == nil {
return nil
}
return & e . EnterpriseMeta
}
func ( e * ServiceSplitterConfigEntry ) ListRelatedServices ( ) [ ] ServiceID {
found := make ( map [ ServiceID ] struct { } )
2019-06-27 17:37:43 +00:00
2020-02-06 15:52:25 +00:00
svcID := NewServiceID ( e . Name , & e . EnterpriseMeta )
2019-06-27 17:37:43 +00:00
for _ , split := range e . Splits {
2020-02-06 15:52:25 +00:00
splitID := NewServiceID ( defaultIfEmpty ( split . Service , e . Name ) , split . GetEnterpriseMeta ( & e . EnterpriseMeta ) )
if splitID != svcID {
found [ splitID ] = struct { } { }
2019-06-27 17:37:43 +00:00
}
}
2019-07-24 01:50:23 +00:00
if len ( found ) == 0 {
return nil
}
2020-01-24 15:04:58 +00:00
out := make ( [ ] ServiceID , 0 , len ( found ) )
2019-06-27 17:37:43 +00:00
for svc , _ := range found {
out = append ( out , svc )
}
2020-01-24 15:04:58 +00:00
sort . Slice ( out , func ( i , j int ) bool {
return out [ i ] . EnterpriseMeta . LessThan ( & out [ j ] . EnterpriseMeta ) ||
out [ i ] . ID < out [ j ] . ID
} )
2019-06-27 17:37:43 +00:00
return out
}
// ServiceSplit defines how much traffic to send to which set of service
// instances during a traffic split.
type ServiceSplit struct {
// A value between 0 and 100 reflecting what portion of traffic should be
// directed to this split.
//
// The smallest representable weight is 1/10000 or .01%
//
// If the split is within epsilon of 100 then the remainder is attributed
// to the FIRST split.
Weight float32
// Service is the service to resolve instead of the default (optional).
Service string ` json:",omitempty" `
// ServiceSubset is a named subset of the given service to resolve instead
// of one defined as that service's DefaultSubset. If empty the default
// subset is used (optional).
//
// If this field is specified then this route is ineligible for further
// splitting.
2020-05-27 18:28:28 +00:00
ServiceSubset string ` json:",omitempty" alias:"service_subset" `
2019-06-27 17:37:43 +00:00
// Namespace is the namespace to resolve the service from instead of the
// current namespace. If empty the current namespace is assumed (optional).
//
// If this field is specified then this route is ineligible for further
// splitting.
Namespace string ` json:",omitempty" `
}
// ServiceResolverConfigEntry defines which instances of a service should
// satisfy discovery requests for a given named service.
//
// This config entry represents the next hop of the discovery chain after
// splitting. If no resolver config is defined the chain assumes 100% of
// traffic goes to the healthy instances of the default service in the current
// datacenter+namespace and discovery terminates.
//
// Resolver configs are recursively collected while walking the chain.
//
// Resolver config entries will be valid for services defined with any protocol
// (in centralized configuration).
type ServiceResolverConfigEntry struct {
Kind string
Name string
// DefaultSubset is the subset to use when no explicit subset is
// requested. If empty the unnamed subset is used.
2020-05-27 18:28:28 +00:00
DefaultSubset string ` json:",omitempty" alias:"default_subset" `
2019-06-27 17:37:43 +00:00
// Subsets is a map of subset name to subset definition for all
// usable named subsets of this service. The map key is the name
// of the subset and all names must be valid DNS subdomain elements
// so they can be used in SNI FQDN headers for the Connect Gateways
// feature.
//
// This may be empty, in which case only the unnamed default subset
// will be usable.
Subsets map [ string ] ServiceResolverSubset ` json:",omitempty" `
// Redirect is a service/subset/datacenter/namespace to resolve
// instead of the requested service (optional).
//
// When configured, all occurrences of this resolver in any discovery
// chain evaluation will be substituted for the supplied redirect
// EXCEPT when the redirect has already been applied.
//
// When substituting the supplied redirect into the discovery chain
// all other fields beside Kind/Name/Redirect will be ignored.
Redirect * ServiceResolverRedirect ` json:",omitempty" `
// Failover controls when and how to reroute traffic to an alternate pool
// of service instances.
//
// The map is keyed by the service subset it applies to, and the special
// string "*" is a wildcard that applies to any subset not otherwise
// specified here.
Failover map [ string ] ServiceResolverFailover ` json:",omitempty" `
// ConnectTimeout is the timeout for establishing new network connections
// to this service.
2020-05-27 18:28:28 +00:00
ConnectTimeout time . Duration ` json:",omitempty" alias:"connect_timeout" `
2019-06-27 17:37:43 +00:00
2020-01-24 15:04:58 +00:00
EnterpriseMeta ` hcl:",squash" mapstructure:",squash" `
2019-06-27 17:37:43 +00:00
RaftIndex
}
2019-08-19 20:31:05 +00:00
func ( e * ServiceResolverConfigEntry ) MarshalJSON ( ) ( [ ] byte , error ) {
type Alias ServiceResolverConfigEntry
exported := & struct {
ConnectTimeout string ` json:",omitempty" `
* Alias
} {
ConnectTimeout : e . ConnectTimeout . String ( ) ,
Alias : ( * Alias ) ( e ) ,
}
if e . ConnectTimeout == 0 {
exported . ConnectTimeout = ""
}
return json . Marshal ( exported )
}
func ( e * ServiceResolverConfigEntry ) UnmarshalJSON ( data [ ] byte ) error {
type Alias ServiceResolverConfigEntry
aux := & struct {
ConnectTimeout string
* Alias
} {
Alias : ( * Alias ) ( e ) ,
}
2019-12-06 16:14:56 +00:00
if err := lib . UnmarshalJSON ( data , & aux ) ; err != nil {
2019-08-19 20:31:05 +00:00
return err
}
var err error
if aux . ConnectTimeout != "" {
if e . ConnectTimeout , err = time . ParseDuration ( aux . ConnectTimeout ) ; err != nil {
return err
}
}
return nil
}
2019-07-01 20:23:36 +00:00
func ( e * ServiceResolverConfigEntry ) SubsetExists ( name string ) bool {
if name == "" {
return true
}
if len ( e . Subsets ) == 0 {
return false
}
_ , ok := e . Subsets [ name ]
return ok
}
2019-06-27 18:38:21 +00:00
func ( e * ServiceResolverConfigEntry ) IsDefault ( ) bool {
return e . DefaultSubset == "" &&
len ( e . Subsets ) == 0 &&
e . Redirect == nil &&
len ( e . Failover ) == 0 &&
e . ConnectTimeout == 0
}
2019-06-27 17:37:43 +00:00
func ( e * ServiceResolverConfigEntry ) GetKind ( ) string {
return ServiceResolver
}
func ( e * ServiceResolverConfigEntry ) GetName ( ) string {
if e == nil {
return ""
}
return e . Name
}
func ( e * ServiceResolverConfigEntry ) Normalize ( ) error {
if e == nil {
return fmt . Errorf ( "config entry is nil" )
}
e . Kind = ServiceResolver
2020-01-24 15:04:58 +00:00
e . EnterpriseMeta . Normalize ( )
2019-06-27 17:37:43 +00:00
return nil
}
func ( e * ServiceResolverConfigEntry ) Validate ( ) error {
if e . Name == "" {
return fmt . Errorf ( "Name is required" )
}
if len ( e . Subsets ) > 0 {
for name , _ := range e . Subsets {
if name == "" {
return fmt . Errorf ( "Subset defined with empty name" )
}
2019-07-24 01:50:23 +00:00
if err := validateServiceSubset ( name ) ; err != nil {
return fmt . Errorf ( "Subset %q is invalid: %v" , name , err )
}
2019-06-27 17:37:43 +00:00
}
}
isSubset := func ( subset string ) bool {
if len ( e . Subsets ) > 0 {
_ , ok := e . Subsets [ subset ]
return ok
}
return false
}
if e . DefaultSubset != "" && ! isSubset ( e . DefaultSubset ) {
return fmt . Errorf ( "DefaultSubset %q is not a valid subset" , e . DefaultSubset )
}
if e . Redirect != nil {
r := e . Redirect
2019-07-01 20:23:36 +00:00
if len ( e . Failover ) > 0 {
return fmt . Errorf ( "Redirect and Failover cannot both be set" )
}
// TODO(rb): prevent subsets and default subsets from being defined?
2019-06-27 17:37:43 +00:00
if r . Service == "" && r . ServiceSubset == "" && r . Namespace == "" && r . Datacenter == "" {
return fmt . Errorf ( "Redirect is empty" )
}
if r . Service == "" {
if r . ServiceSubset != "" {
return fmt . Errorf ( "Redirect.ServiceSubset defined without Redirect.Service" )
}
if r . Namespace != "" {
return fmt . Errorf ( "Redirect.Namespace defined without Redirect.Service" )
}
} else if r . Service == e . Name {
if r . ServiceSubset != "" && ! isSubset ( r . ServiceSubset ) {
return fmt . Errorf ( "Redirect.ServiceSubset %q is not a valid subset of %q" , r . ServiceSubset , r . Service )
}
}
}
if len ( e . Failover ) > 0 {
for subset , f := range e . Failover {
if subset != "*" && ! isSubset ( subset ) {
return fmt . Errorf ( "Bad Failover[%q]: not a valid subset" , subset )
}
2019-07-01 20:23:36 +00:00
if f . Service == "" && f . ServiceSubset == "" && f . Namespace == "" && len ( f . Datacenters ) == 0 {
return fmt . Errorf ( "Bad Failover[%q] one of Service, ServiceSubset, Namespace, or Datacenters is required" , subset )
2019-06-27 17:37:43 +00:00
}
if f . ServiceSubset != "" {
if f . Service == "" || f . Service == e . Name {
if ! isSubset ( f . ServiceSubset ) {
return fmt . Errorf ( "Bad Failover[%q].ServiceSubset %q is not a valid subset of %q" , subset , f . ServiceSubset , f . Service )
}
}
}
for _ , dc := range f . Datacenters {
if dc == "" {
return fmt . Errorf ( "Bad Failover[%q].Datacenters: found empty datacenter" , subset )
}
}
}
}
if e . ConnectTimeout < 0 {
return fmt . Errorf ( "Bad ConnectTimeout '%s', must be >= 0" , e . ConnectTimeout )
}
return nil
}
func ( e * ServiceResolverConfigEntry ) CanRead ( rule acl . Authorizer ) bool {
return canReadDiscoveryChain ( e , rule )
}
func ( e * ServiceResolverConfigEntry ) CanWrite ( rule acl . Authorizer ) bool {
return canWriteDiscoveryChain ( e , rule )
}
func ( e * ServiceResolverConfigEntry ) GetRaftIndex ( ) * RaftIndex {
if e == nil {
return & RaftIndex { }
}
return & e . RaftIndex
}
2020-01-24 15:04:58 +00:00
func ( e * ServiceResolverConfigEntry ) GetEnterpriseMeta ( ) * EnterpriseMeta {
if e == nil {
return nil
}
return & e . EnterpriseMeta
}
func ( e * ServiceResolverConfigEntry ) ListRelatedServices ( ) [ ] ServiceID {
found := make ( map [ ServiceID ] struct { } )
2019-06-27 17:37:43 +00:00
2020-02-06 15:52:25 +00:00
svcID := NewServiceID ( e . Name , & e . EnterpriseMeta )
2019-06-27 17:37:43 +00:00
if e . Redirect != nil {
2020-02-06 15:52:25 +00:00
redirectID := NewServiceID ( defaultIfEmpty ( e . Redirect . Service , e . Name ) , e . Redirect . GetEnterpriseMeta ( & e . EnterpriseMeta ) )
if redirectID != svcID {
found [ redirectID ] = struct { } { }
2019-06-27 17:37:43 +00:00
}
}
if len ( e . Failover ) > 0 {
for _ , failover := range e . Failover {
2020-02-06 15:52:25 +00:00
failoverID := NewServiceID ( defaultIfEmpty ( failover . Service , e . Name ) , failover . GetEnterpriseMeta ( & e . EnterpriseMeta ) )
if failoverID != svcID {
found [ failoverID ] = struct { } { }
2019-06-27 17:37:43 +00:00
}
}
}
2019-07-24 01:50:23 +00:00
if len ( found ) == 0 {
return nil
}
2020-01-24 15:04:58 +00:00
out := make ( [ ] ServiceID , 0 , len ( found ) )
2019-06-27 17:37:43 +00:00
for svc , _ := range found {
out = append ( out , svc )
}
2020-01-24 15:04:58 +00:00
sort . Slice ( out , func ( i , j int ) bool {
return out [ i ] . EnterpriseMeta . LessThan ( & out [ j ] . EnterpriseMeta ) ||
out [ i ] . ID < out [ j ] . ID
} )
2019-06-27 17:37:43 +00:00
return out
}
// ServiceResolverSubset defines a way to select a portion of the Consul
// catalog during service discovery. Anything that affects the ultimate catalog
// query performed OR post-processing on the results of that sort of query
// should be defined here.
type ServiceResolverSubset struct {
// Filter specifies the go-bexpr filter expression to be used for selecting
// instances of the requested service.
Filter string ` json:",omitempty" `
// OnlyPassing - Specifies the behavior of the resolver's health check
// filtering. If this is set to false, the results will include instances
// with checks in the passing as well as the warning states. If this is set
// to true, only instances with checks in the passing state will be
// returned. (behaves identically to the similarly named field on prepared
// queries).
2020-05-27 18:28:28 +00:00
OnlyPassing bool ` json:",omitempty" alias:"only_passing" `
2019-06-27 17:37:43 +00:00
}
type ServiceResolverRedirect struct {
// Service is a service to resolve instead of the current service
// (optional).
Service string ` json:",omitempty" `
// ServiceSubset is a named subset of the given service to resolve instead
// of one defined as that service's DefaultSubset If empty the default
// subset is used (optional).
//
// If this is specified at least one of Service, Datacenter, or Namespace
// should be configured.
2020-05-27 18:28:28 +00:00
ServiceSubset string ` json:",omitempty" alias:"service_subset" `
2019-06-27 17:37:43 +00:00
// Namespace is the namespace to resolve the service from instead of the
// current one (optional).
Namespace string ` json:",omitempty" `
// Datacenter is the datacenter to resolve the service from instead of the
// current one (optional).
Datacenter string ` json:",omitempty" `
}
// There are some restrictions on what is allowed in here:
//
2019-08-02 20:34:54 +00:00
// - Service, ServiceSubset, Namespace, and Datacenters cannot all be
2019-06-27 17:37:43 +00:00
// empty at once.
//
type ServiceResolverFailover struct {
// Service is the service to resolve instead of the default as the failover
// group of instances (optional).
//
// This is a DESTINATION during failover.
Service string ` json:",omitempty" `
// ServiceSubset is the named subset of the requested service to resolve as
// the failover group of instances. If empty the default subset for the
// requested service is used (optional).
//
// This is a DESTINATION during failover.
2020-05-27 18:28:28 +00:00
ServiceSubset string ` json:",omitempty" alias:"service_subset" `
2019-06-27 17:37:43 +00:00
// Namespace is the namespace to resolve the requested service from to form
// the failover group of instances. If empty the current namespace is used
// (optional).
//
// This is a DESTINATION during failover.
Namespace string ` json:",omitempty" `
2019-08-02 20:34:54 +00:00
// Datacenters is a fixed list of datacenters to try. We never try a
// datacenter multiple times, so those are subtracted from this list before
// proceeding.
2019-06-27 17:37:43 +00:00
//
// This is a DESTINATION during failover.
Datacenters [ ] string ` json:",omitempty" `
}
type discoveryChainConfigEntry interface {
ConfigEntry
// ListRelatedServices returns a list of other names of services referenced
// in this config entry.
2020-01-24 15:04:58 +00:00
ListRelatedServices ( ) [ ] ServiceID
2019-06-27 17:37:43 +00:00
}
2020-01-24 15:04:58 +00:00
func canReadDiscoveryChain ( entry discoveryChainConfigEntry , authz acl . Authorizer ) bool {
var authzContext acl . AuthorizerContext
entry . GetEnterpriseMeta ( ) . FillAuthzContext ( & authzContext )
return authz . ServiceRead ( entry . GetName ( ) , & authzContext ) == acl . Allow
2019-06-27 18:38:21 +00:00
}
2019-06-27 17:37:43 +00:00
2019-06-27 18:38:21 +00:00
func canWriteDiscoveryChain ( entry discoveryChainConfigEntry , rule acl . Authorizer ) bool {
2020-02-06 15:52:25 +00:00
entryID := NewServiceID ( entry . GetName ( ) , entry . GetEnterpriseMeta ( ) )
2020-01-24 15:04:58 +00:00
var authzContext acl . AuthorizerContext
2020-02-06 15:52:25 +00:00
entryID . FillAuthzContext ( & authzContext )
2020-01-24 15:04:58 +00:00
2019-06-27 17:37:43 +00:00
name := entry . GetName ( )
2020-01-24 15:04:58 +00:00
if rule . ServiceWrite ( name , & authzContext ) != acl . Allow {
2019-06-27 17:37:43 +00:00
return false
}
for _ , svc := range entry . ListRelatedServices ( ) {
2020-02-06 15:52:25 +00:00
if entryID == svc {
2019-06-27 17:37:43 +00:00
continue
}
2019-06-27 18:38:21 +00:00
2020-01-24 15:04:58 +00:00
svc . FillAuthzContext ( & authzContext )
2019-06-27 18:38:21 +00:00
// You only need read on related services to redirect traffic flow for
// your own service.
2020-01-24 15:04:58 +00:00
if rule . ServiceRead ( svc . ID , & authzContext ) != acl . Allow {
2019-06-27 17:37:43 +00:00
return false
}
}
return true
}
2019-06-27 18:38:21 +00:00
// DiscoveryChainConfigEntries wraps just the raw cross-referenced config
// entries. None of these are defaulted.
type DiscoveryChainConfigEntries struct {
2020-01-24 15:04:58 +00:00
Routers map [ ServiceID ] * ServiceRouterConfigEntry
Splitters map [ ServiceID ] * ServiceSplitterConfigEntry
Resolvers map [ ServiceID ] * ServiceResolverConfigEntry
Services map [ ServiceID ] * ServiceConfigEntry
2019-07-02 16:01:17 +00:00
GlobalProxy * ProxyConfigEntry
2019-06-27 18:38:21 +00:00
}
2019-07-12 19:16:21 +00:00
func NewDiscoveryChainConfigEntries ( ) * DiscoveryChainConfigEntries {
return & DiscoveryChainConfigEntries {
2020-01-24 15:04:58 +00:00
Routers : make ( map [ ServiceID ] * ServiceRouterConfigEntry ) ,
Splitters : make ( map [ ServiceID ] * ServiceSplitterConfigEntry ) ,
Resolvers : make ( map [ ServiceID ] * ServiceResolverConfigEntry ) ,
Services : make ( map [ ServiceID ] * ServiceConfigEntry ) ,
2019-07-12 19:16:21 +00:00
}
}
2020-01-24 15:04:58 +00:00
func ( e * DiscoveryChainConfigEntries ) GetRouter ( sid ServiceID ) * ServiceRouterConfigEntry {
2019-06-27 18:38:21 +00:00
if e . Routers != nil {
2020-01-24 15:04:58 +00:00
return e . Routers [ sid ]
2019-06-27 18:38:21 +00:00
}
return nil
}
2019-06-27 17:37:43 +00:00
2020-01-24 15:04:58 +00:00
func ( e * DiscoveryChainConfigEntries ) GetSplitter ( sid ServiceID ) * ServiceSplitterConfigEntry {
2019-06-27 18:38:21 +00:00
if e . Splitters != nil {
2020-01-24 15:04:58 +00:00
return e . Splitters [ sid ]
2019-06-27 17:37:43 +00:00
}
2019-06-27 18:38:21 +00:00
return nil
}
2020-01-24 15:04:58 +00:00
func ( e * DiscoveryChainConfigEntries ) GetResolver ( sid ServiceID ) * ServiceResolverConfigEntry {
2019-06-27 18:38:21 +00:00
if e . Resolvers != nil {
2020-01-24 15:04:58 +00:00
return e . Resolvers [ sid ]
2019-06-27 18:38:21 +00:00
}
return nil
}
2020-01-24 15:04:58 +00:00
func ( e * DiscoveryChainConfigEntries ) GetService ( sid ServiceID ) * ServiceConfigEntry {
2019-06-27 18:38:21 +00:00
if e . Services != nil {
2020-01-24 15:04:58 +00:00
return e . Services [ sid ]
2019-06-27 18:38:21 +00:00
}
return nil
}
// AddRouters adds router configs. Convenience function for testing.
func ( e * DiscoveryChainConfigEntries ) AddRouters ( entries ... * ServiceRouterConfigEntry ) {
if e . Routers == nil {
2020-01-24 15:04:58 +00:00
e . Routers = make ( map [ ServiceID ] * ServiceRouterConfigEntry )
2019-06-27 18:38:21 +00:00
}
for _ , entry := range entries {
2020-01-24 15:04:58 +00:00
e . Routers [ NewServiceID ( entry . Name , & entry . EnterpriseMeta ) ] = entry
2019-06-27 18:38:21 +00:00
}
}
// AddSplitters adds splitter configs. Convenience function for testing.
func ( e * DiscoveryChainConfigEntries ) AddSplitters ( entries ... * ServiceSplitterConfigEntry ) {
if e . Splitters == nil {
2020-01-24 15:04:58 +00:00
e . Splitters = make ( map [ ServiceID ] * ServiceSplitterConfigEntry )
2019-06-27 18:38:21 +00:00
}
for _ , entry := range entries {
2020-01-24 15:04:58 +00:00
e . Splitters [ NewServiceID ( entry . Name , entry . GetEnterpriseMeta ( ) ) ] = entry
2019-06-27 18:38:21 +00:00
}
}
// AddResolvers adds resolver configs. Convenience function for testing.
func ( e * DiscoveryChainConfigEntries ) AddResolvers ( entries ... * ServiceResolverConfigEntry ) {
if e . Resolvers == nil {
2020-01-24 15:04:58 +00:00
e . Resolvers = make ( map [ ServiceID ] * ServiceResolverConfigEntry )
2019-06-27 18:38:21 +00:00
}
for _ , entry := range entries {
2020-01-24 15:04:58 +00:00
e . Resolvers [ NewServiceID ( entry . Name , entry . GetEnterpriseMeta ( ) ) ] = entry
2019-06-27 18:38:21 +00:00
}
}
// AddServices adds service configs. Convenience function for testing.
func ( e * DiscoveryChainConfigEntries ) AddServices ( entries ... * ServiceConfigEntry ) {
if e . Services == nil {
2020-01-24 15:04:58 +00:00
e . Services = make ( map [ ServiceID ] * ServiceConfigEntry )
2019-06-27 18:38:21 +00:00
}
for _ , entry := range entries {
2020-01-24 15:04:58 +00:00
e . Services [ NewServiceID ( entry . Name , entry . GetEnterpriseMeta ( ) ) ] = entry
2019-06-27 18:38:21 +00:00
}
}
2019-07-12 19:16:21 +00:00
// AddEntries adds generic configs. Convenience function for testing. Panics on
// operator error.
func ( e * DiscoveryChainConfigEntries ) AddEntries ( entries ... ConfigEntry ) {
for _ , entry := range entries {
switch entry . GetKind ( ) {
case ServiceRouter :
e . AddRouters ( entry . ( * ServiceRouterConfigEntry ) )
case ServiceSplitter :
e . AddSplitters ( entry . ( * ServiceSplitterConfigEntry ) )
case ServiceResolver :
e . AddResolvers ( entry . ( * ServiceResolverConfigEntry ) )
case ServiceDefaults :
e . AddServices ( entry . ( * ServiceConfigEntry ) )
case ProxyDefaults :
if entry . GetName ( ) != ProxyConfigGlobal {
panic ( "the only supported proxy-defaults name is '" + ProxyConfigGlobal + "'" )
}
e . GlobalProxy = entry . ( * ProxyConfigEntry )
default :
panic ( "unhandled config entry kind: " + entry . GetKind ( ) )
}
}
}
2019-06-27 18:38:21 +00:00
func ( e * DiscoveryChainConfigEntries ) IsEmpty ( ) bool {
2019-07-02 16:01:17 +00:00
return e . IsChainEmpty ( ) && len ( e . Services ) == 0 && e . GlobalProxy == nil
2019-06-27 18:38:21 +00:00
}
func ( e * DiscoveryChainConfigEntries ) IsChainEmpty ( ) bool {
return len ( e . Routers ) == 0 && len ( e . Splitters ) == 0 && len ( e . Resolvers ) == 0
2019-06-27 17:37:43 +00:00
}
2019-07-01 20:23:36 +00:00
2019-07-02 03:10:51 +00:00
// DiscoveryChainRequest is used when requesting the discovery chain for a
// service.
type DiscoveryChainRequest struct {
2019-08-02 03:03:34 +00:00
Name string
EvaluateInDatacenter string
EvaluateInNamespace string
2019-07-02 03:10:51 +00:00
2019-08-02 03:03:34 +00:00
// OverrideMeshGateway allows for the mesh gateway setting to be overridden
// for any resolver in the compiled chain.
OverrideMeshGateway MeshGatewayConfig
// OverrideProtocol allows for the final protocol for the chain to be
// altered.
//
// - If the chain ordinarily would be TCP and an L7 protocol is passed here
// the chain will not include Routers or Splitters.
//
// - If the chain ordinarily would be L7 and TCP is passed here the chain
// will not include Routers or Splitters.
OverrideProtocol string
// OverrideConnectTimeout allows for the ConnectTimeout setting to be
// overridden for any resolver in the compiled chain.
OverrideConnectTimeout time . Duration
Datacenter string // where to route the RPC
2019-07-02 03:10:51 +00:00
QueryOptions
}
func ( r * DiscoveryChainRequest ) RequestDatacenter ( ) string {
return r . Datacenter
}
func ( r * DiscoveryChainRequest ) CacheInfo ( ) cache . RequestInfo {
info := cache . RequestInfo {
Token : r . Token ,
Datacenter : r . Datacenter ,
MinIndex : r . MinQueryIndex ,
Timeout : r . MaxQueryTime ,
MaxAge : r . MaxAge ,
MustRevalidate : r . MustRevalidate ,
}
v , err := hashstructure . Hash ( struct {
2019-08-02 03:03:34 +00:00
Name string
EvaluateInDatacenter string
EvaluateInNamespace string
OverrideMeshGateway MeshGatewayConfig
OverrideProtocol string
OverrideConnectTimeout time . Duration
2019-07-02 03:10:51 +00:00
} {
2019-08-02 03:03:34 +00:00
Name : r . Name ,
EvaluateInDatacenter : r . EvaluateInDatacenter ,
EvaluateInNamespace : r . EvaluateInNamespace ,
OverrideMeshGateway : r . OverrideMeshGateway ,
OverrideProtocol : r . OverrideProtocol ,
OverrideConnectTimeout : r . OverrideConnectTimeout ,
2019-07-02 03:10:51 +00:00
} , nil )
if err == nil {
// If there is an error, we don't set the key. A blank key forces
// no cache for this request so the request is forwarded directly
// to the server.
info . Key = strconv . FormatUint ( v , 10 )
}
return info
}
type DiscoveryChainResponse struct {
2019-08-02 20:34:54 +00:00
Chain * CompiledDiscoveryChain
2019-07-02 03:10:51 +00:00
QueryMeta
}
2019-07-01 20:23:36 +00:00
type ConfigEntryGraphError struct {
// one of Message or Err should be set
Message string
Err error
}
func ( e * ConfigEntryGraphError ) Error ( ) string {
if e . Err != nil {
return e . Err . Error ( )
}
return e . Message
}
2019-07-24 01:50:23 +00:00
var (
validServiceSubset = regexp . MustCompile ( ` ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ` )
serviceSubsetMaxLength = 63
)
// validateServiceSubset checks if the provided name can be used as an service
// subset. Because these are used in SNI headers they must a DNS label per
// RFC-1035/RFC-1123.
func validateServiceSubset ( subset string ) error {
if subset == "" || len ( subset ) > serviceSubsetMaxLength {
return fmt . Errorf ( "must be non-empty and 63 characters or fewer" )
}
if ! validServiceSubset . MatchString ( subset ) {
return fmt . Errorf ( "must be 63 characters or fewer, begin or end with lower case alphanumeric characters, and contain lower case alphanumeric characters or '-' in between" )
}
return nil
}
2020-02-06 15:52:25 +00:00
func defaultIfEmpty ( val , defaultVal string ) string {
if val != "" {
return val
}
return defaultVal
}