You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
consul/agent/structs/config_entry_discoverychain.go

1215 lines
34 KiB

package structs
import (
"encoding/json"
"fmt"
"math"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/cache"
"github.com/hashicorp/consul/lib"
"github.com/mitchellh/hashstructure"
)
// 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
EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
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
e.EnterpriseMeta.Normalize()
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])
}
if route.Destination != nil && route.Destination.Namespace == "" {
route.Destination.Namespace = e.EnterpriseMeta.NamespaceOrDefault()
}
}
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?
for i, route := range e.Routes {
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)
}
}
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)
}
}
if route.Match.HTTP.PathRegex != "" {
pathParts++
}
if pathParts > 1 {
return fmt.Errorf("Route[%d] should only contain at most one of PathExact, PathPrefix, or PathRegex", i)
}
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)
}
}
for j, qm := range route.Match.HTTP.QueryParam {
if qm.Name == "" {
return fmt.Errorf("Route[%d] QueryParam[%d] missing required Name field", i, j)
}
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)
}
}
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{}{}
}
}
}
if route.Destination != nil {
if route.Destination.PrefixRewrite != "" && !eligibleForPrefixRewrite {
return fmt.Errorf("Route[%d] cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix", i)
}
}
}
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
}
func (e *ServiceRouterConfigEntry) ListRelatedServices() []ServiceID {
found := make(map[ServiceID]struct{})
// We always inject a default catch-all route to the same service as the router.
svcID := NewServiceID(e.Name, &e.EnterpriseMeta)
found[svcID] = struct{}{}
for _, route := range e.Routes {
if route.Destination != nil {
destID := NewServiceID(defaultIfEmpty(route.Destination.Service, e.Name), route.Destination.GetEnterpriseMeta(&e.EnterpriseMeta))
if destID != svcID {
found[destID] = struct{}{}
}
}
}
if len(found) == 0 {
return nil
}
out := make([]ServiceID, 0, len(found))
for svc, _ := range found {
out = append(out, svc)
}
sort.Slice(out, func(i, j int) bool {
return out[i].EnterpriseMeta.LessThan(&out[j].EnterpriseMeta) ||
out[i].ID < out[j].ID
})
return out
}
func (e *ServiceRouterConfigEntry) GetEnterpriseMeta() *EnterpriseMeta {
if e == nil {
return nil
}
return &e.EnterpriseMeta
}
// 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.
}
func (m *ServiceRouteMatch) IsEmpty() bool {
return m.HTTP == nil || m.HTTP.IsEmpty()
}
// ServiceRouteHTTPMatch is a set of http-specific match criteria.
type ServiceRouteHTTPMatch struct {
PathExact string `json:",omitempty"`
PathPrefix string `json:",omitempty"`
PathRegex string `json:",omitempty"`
Header []ServiceRouteHTTPMatchHeader `json:",omitempty"`
QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty"`
Methods []string `json:",omitempty"`
}
func (m *ServiceRouteHTTPMatch) IsEmpty() bool {
return m.PathExact == "" &&
m.PathPrefix == "" &&
m.PathRegex == "" &&
len(m.Header) == 0 &&
len(m.QueryParam) == 0 &&
len(m.Methods) == 0
}
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 {
Name string
Present bool `json:",omitempty"`
Exact string `json:",omitempty"`
Regex string `json:",omitempty"`
}
// 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.
ServiceSubset string `json:",omitempty"`
// 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.
PrefixRewrite string `json:",omitempty"`
// RequestTimeout is the total amount of time permitted for the entire
// downstream request (and retries) to be processed.
RequestTimeout time.Duration `json:",omitempty"`
// NumRetries is the number of times to retry the request when a retryable
// result occurs. This seems fairly proxy agnostic.
NumRetries uint32 `json:",omitempty"`
// 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.
RetryOnConnectFailure bool `json:",omitempty"`
// RetryOnStatusCodes is a flat list of http response status codes that are
// eligible for retry. This again should be feasible in any sane proxy.
RetryOnStatusCodes []uint32 `json:",omitempty"`
}
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),
}
if err := lib.UnmarshalJSON(data, &aux); err != nil {
return err
}
var err error
if aux.RequestTimeout != "" {
if e.RequestTimeout, err = time.ParseDuration(aux.RequestTimeout); err != nil {
return err
}
}
return nil
}
func (d *ServiceRouteDestination) HasRetryFeatures() bool {
return d.NumRetries > 0 || d.RetryOnConnectFailure || len(d.RetryOnStatusCodes) > 0
}
// 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
EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
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%
e.EnterpriseMeta.Normalize()
if len(e.Splits) > 0 {
for i, split := range e.Splits {
if split.Namespace == "" {
split.Namespace = e.EnterpriseMeta.NamespaceOrDefault()
}
e.Splits[i].Weight = NormalizeServiceSplitWeight(split.Weight)
}
}
return nil
}
func NormalizeServiceSplitWeight(weight float32) float32 {
weightScaled := scaleWeight(weight)
return float32(float32(weightScaled) / 100.0)
}
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
}
func (e *ServiceSplitterConfigEntry) GetEnterpriseMeta() *EnterpriseMeta {
if e == nil {
return nil
}
return &e.EnterpriseMeta
}
func (e *ServiceSplitterConfigEntry) ListRelatedServices() []ServiceID {
found := make(map[ServiceID]struct{})
svcID := NewServiceID(e.Name, &e.EnterpriseMeta)
for _, split := range e.Splits {
splitID := NewServiceID(defaultIfEmpty(split.Service, e.Name), split.GetEnterpriseMeta(&e.EnterpriseMeta))
if splitID != svcID {
found[splitID] = struct{}{}
}
}
if len(found) == 0 {
return nil
}
out := make([]ServiceID, 0, len(found))
for svc, _ := range found {
out = append(out, svc)
}
sort.Slice(out, func(i, j int) bool {
return out[i].EnterpriseMeta.LessThan(&out[j].EnterpriseMeta) ||
out[i].ID < out[j].ID
})
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.
ServiceSubset string `json:",omitempty"`
// 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.
DefaultSubset string `json:",omitempty"`
// 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.
ConnectTimeout time.Duration `json:",omitempty"`
EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
RaftIndex
}
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),
}
if err := lib.UnmarshalJSON(data, &aux); err != nil {
return err
}
var err error
if aux.ConnectTimeout != "" {
if e.ConnectTimeout, err = time.ParseDuration(aux.ConnectTimeout); err != nil {
return err
}
}
return nil
}
func (e *ServiceResolverConfigEntry) SubsetExists(name string) bool {
if name == "" {
return true
}
if len(e.Subsets) == 0 {
return false
}
_, ok := e.Subsets[name]
return ok
}
func (e *ServiceResolverConfigEntry) IsDefault() bool {
return e.DefaultSubset == "" &&
len(e.Subsets) == 0 &&
e.Redirect == nil &&
len(e.Failover) == 0 &&
e.ConnectTimeout == 0
}
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
e.EnterpriseMeta.Normalize()
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")
}
if err := validateServiceSubset(name); err != nil {
return fmt.Errorf("Subset %q is invalid: %v", name, err)
}
}
}
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
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?
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)
}
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)
}
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
}
func (e *ServiceResolverConfigEntry) GetEnterpriseMeta() *EnterpriseMeta {
if e == nil {
return nil
}
return &e.EnterpriseMeta
}
func (e *ServiceResolverConfigEntry) ListRelatedServices() []ServiceID {
found := make(map[ServiceID]struct{})
svcID := NewServiceID(e.Name, &e.EnterpriseMeta)
if e.Redirect != nil {
redirectID := NewServiceID(defaultIfEmpty(e.Redirect.Service, e.Name), e.Redirect.GetEnterpriseMeta(&e.EnterpriseMeta))
if redirectID != svcID {
found[redirectID] = struct{}{}
}
}
if len(e.Failover) > 0 {
for _, failover := range e.Failover {
failoverID := NewServiceID(defaultIfEmpty(failover.Service, e.Name), failover.GetEnterpriseMeta(&e.EnterpriseMeta))
if failoverID != svcID {
found[failoverID] = struct{}{}
}
}
}
if len(found) == 0 {
return nil
}
out := make([]ServiceID, 0, len(found))
for svc, _ := range found {
out = append(out, svc)
}
sort.Slice(out, func(i, j int) bool {
return out[i].EnterpriseMeta.LessThan(&out[j].EnterpriseMeta) ||
out[i].ID < out[j].ID
})
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).
OnlyPassing bool `json:",omitempty"`
}
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.
ServiceSubset string `json:",omitempty"`
// 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:
//
// - Service, ServiceSubset, Namespace, and Datacenters cannot all be
// 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.
ServiceSubset string `json:",omitempty"`
// 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"`
// 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.
//
// 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.
ListRelatedServices() []ServiceID
}
func canReadDiscoveryChain(entry discoveryChainConfigEntry, authz acl.Authorizer) bool {
var authzContext acl.AuthorizerContext
entry.GetEnterpriseMeta().FillAuthzContext(&authzContext)
return authz.ServiceRead(entry.GetName(), &authzContext) == acl.Allow
}
func canWriteDiscoveryChain(entry discoveryChainConfigEntry, rule acl.Authorizer) bool {
entryID := NewServiceID(entry.GetName(), entry.GetEnterpriseMeta())
var authzContext acl.AuthorizerContext
entryID.FillAuthzContext(&authzContext)
name := entry.GetName()
if rule.ServiceWrite(name, &authzContext) != acl.Allow {
return false
}
for _, svc := range entry.ListRelatedServices() {
if entryID == svc {
continue
}
svc.FillAuthzContext(&authzContext)
// You only need read on related services to redirect traffic flow for
// your own service.
if rule.ServiceRead(svc.ID, &authzContext) != acl.Allow {
return false
}
}
return true
}
// DiscoveryChainConfigEntries wraps just the raw cross-referenced config
// entries. None of these are defaulted.
type DiscoveryChainConfigEntries struct {
Routers map[ServiceID]*ServiceRouterConfigEntry
Splitters map[ServiceID]*ServiceSplitterConfigEntry
Resolvers map[ServiceID]*ServiceResolverConfigEntry
Services map[ServiceID]*ServiceConfigEntry
GlobalProxy *ProxyConfigEntry
}
func NewDiscoveryChainConfigEntries() *DiscoveryChainConfigEntries {
return &DiscoveryChainConfigEntries{
Routers: make(map[ServiceID]*ServiceRouterConfigEntry),
Splitters: make(map[ServiceID]*ServiceSplitterConfigEntry),
Resolvers: make(map[ServiceID]*ServiceResolverConfigEntry),
Services: make(map[ServiceID]*ServiceConfigEntry),
}
}
func (e *DiscoveryChainConfigEntries) GetRouter(sid ServiceID) *ServiceRouterConfigEntry {
if e.Routers != nil {
return e.Routers[sid]
}
return nil
}
func (e *DiscoveryChainConfigEntries) GetSplitter(sid ServiceID) *ServiceSplitterConfigEntry {
if e.Splitters != nil {
return e.Splitters[sid]
}
return nil
}
func (e *DiscoveryChainConfigEntries) GetResolver(sid ServiceID) *ServiceResolverConfigEntry {
if e.Resolvers != nil {
return e.Resolvers[sid]
}
return nil
}
func (e *DiscoveryChainConfigEntries) GetService(sid ServiceID) *ServiceConfigEntry {
if e.Services != nil {
return e.Services[sid]
}
return nil
}
// AddRouters adds router configs. Convenience function for testing.
func (e *DiscoveryChainConfigEntries) AddRouters(entries ...*ServiceRouterConfigEntry) {
if e.Routers == nil {
e.Routers = make(map[ServiceID]*ServiceRouterConfigEntry)
}
for _, entry := range entries {
e.Routers[NewServiceID(entry.Name, &entry.EnterpriseMeta)] = entry
}
}
// AddSplitters adds splitter configs. Convenience function for testing.
func (e *DiscoveryChainConfigEntries) AddSplitters(entries ...*ServiceSplitterConfigEntry) {
if e.Splitters == nil {
e.Splitters = make(map[ServiceID]*ServiceSplitterConfigEntry)
}
for _, entry := range entries {
e.Splitters[NewServiceID(entry.Name, entry.GetEnterpriseMeta())] = entry
}
}
// AddResolvers adds resolver configs. Convenience function for testing.
func (e *DiscoveryChainConfigEntries) AddResolvers(entries ...*ServiceResolverConfigEntry) {
if e.Resolvers == nil {
e.Resolvers = make(map[ServiceID]*ServiceResolverConfigEntry)
}
for _, entry := range entries {
e.Resolvers[NewServiceID(entry.Name, entry.GetEnterpriseMeta())] = entry
}
}
// AddServices adds service configs. Convenience function for testing.
func (e *DiscoveryChainConfigEntries) AddServices(entries ...*ServiceConfigEntry) {
if e.Services == nil {
e.Services = make(map[ServiceID]*ServiceConfigEntry)
}
for _, entry := range entries {
e.Services[NewServiceID(entry.Name, entry.GetEnterpriseMeta())] = entry
}
}
// 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())
}
}
}
func (e *DiscoveryChainConfigEntries) IsEmpty() bool {
return e.IsChainEmpty() && len(e.Services) == 0 && e.GlobalProxy == nil
}
func (e *DiscoveryChainConfigEntries) IsChainEmpty() bool {
return len(e.Routers) == 0 && len(e.Splitters) == 0 && len(e.Resolvers) == 0
}
// DiscoveryChainRequest is used when requesting the discovery chain for a
// service.
type DiscoveryChainRequest struct {
connect: reconcile how upstream configuration works with discovery chains (#6225) * connect: reconcile how upstream configuration works with discovery chains The following upstream config fields for connect sidecars sanely integrate into discovery chain resolution: - Destination Namespace/Datacenter: Compilation occurs locally but using different default values for namespaces and datacenters. The xDS clusters that are created are named as they normally would be. - Mesh Gateway Mode (single upstream): If set this value overrides any value computed for any resolver for the entire discovery chain. The xDS clusters that are created may be named differently (see below). - Mesh Gateway Mode (whole sidecar): If set this value overrides any value computed for any resolver for the entire discovery chain. If this is specifically overridden for a single upstream this value is ignored in that case. The xDS clusters that are created may be named differently (see below). - Protocol (in opaque config): If set this value overrides the value computed when evaluating the entire discovery chain. If the normal chain would be TCP or if this override is set to TCP then the result is that we explicitly disable L7 Routing and Splitting. The xDS clusters that are created may be named differently (see below). - Connect Timeout (in opaque config): If set this value overrides the value for any resolver in the entire discovery chain. The xDS clusters that are created may be named differently (see below). If any of the above overrides affect the actual result of compiling the discovery chain (i.e. "tcp" becomes "grpc" instead of being a no-op override to "tcp") then the relevant parameters are hashed and provided to the xDS layer as a prefix for use in naming the Clusters. This is to ensure that if one Upstream discovery chain has no overrides and tangentially needs a cluster named "api.default.XXX", and another Upstream does have overrides for "api.default.XXX" that they won't cross-pollinate against the operator's wishes. Fixes #6159
5 years ago
Name string
EvaluateInDatacenter string
EvaluateInNamespace string
connect: reconcile how upstream configuration works with discovery chains (#6225) * connect: reconcile how upstream configuration works with discovery chains The following upstream config fields for connect sidecars sanely integrate into discovery chain resolution: - Destination Namespace/Datacenter: Compilation occurs locally but using different default values for namespaces and datacenters. The xDS clusters that are created are named as they normally would be. - Mesh Gateway Mode (single upstream): If set this value overrides any value computed for any resolver for the entire discovery chain. The xDS clusters that are created may be named differently (see below). - Mesh Gateway Mode (whole sidecar): If set this value overrides any value computed for any resolver for the entire discovery chain. If this is specifically overridden for a single upstream this value is ignored in that case. The xDS clusters that are created may be named differently (see below). - Protocol (in opaque config): If set this value overrides the value computed when evaluating the entire discovery chain. If the normal chain would be TCP or if this override is set to TCP then the result is that we explicitly disable L7 Routing and Splitting. The xDS clusters that are created may be named differently (see below). - Connect Timeout (in opaque config): If set this value overrides the value for any resolver in the entire discovery chain. The xDS clusters that are created may be named differently (see below). If any of the above overrides affect the actual result of compiling the discovery chain (i.e. "tcp" becomes "grpc" instead of being a no-op override to "tcp") then the relevant parameters are hashed and provided to the xDS layer as a prefix for use in naming the Clusters. This is to ensure that if one Upstream discovery chain has no overrides and tangentially needs a cluster named "api.default.XXX", and another Upstream does have overrides for "api.default.XXX" that they won't cross-pollinate against the operator's wishes. Fixes #6159
5 years ago
// 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
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 {
connect: reconcile how upstream configuration works with discovery chains (#6225) * connect: reconcile how upstream configuration works with discovery chains The following upstream config fields for connect sidecars sanely integrate into discovery chain resolution: - Destination Namespace/Datacenter: Compilation occurs locally but using different default values for namespaces and datacenters. The xDS clusters that are created are named as they normally would be. - Mesh Gateway Mode (single upstream): If set this value overrides any value computed for any resolver for the entire discovery chain. The xDS clusters that are created may be named differently (see below). - Mesh Gateway Mode (whole sidecar): If set this value overrides any value computed for any resolver for the entire discovery chain. If this is specifically overridden for a single upstream this value is ignored in that case. The xDS clusters that are created may be named differently (see below). - Protocol (in opaque config): If set this value overrides the value computed when evaluating the entire discovery chain. If the normal chain would be TCP or if this override is set to TCP then the result is that we explicitly disable L7 Routing and Splitting. The xDS clusters that are created may be named differently (see below). - Connect Timeout (in opaque config): If set this value overrides the value for any resolver in the entire discovery chain. The xDS clusters that are created may be named differently (see below). If any of the above overrides affect the actual result of compiling the discovery chain (i.e. "tcp" becomes "grpc" instead of being a no-op override to "tcp") then the relevant parameters are hashed and provided to the xDS layer as a prefix for use in naming the Clusters. This is to ensure that if one Upstream discovery chain has no overrides and tangentially needs a cluster named "api.default.XXX", and another Upstream does have overrides for "api.default.XXX" that they won't cross-pollinate against the operator's wishes. Fixes #6159
5 years ago
Name string
EvaluateInDatacenter string
EvaluateInNamespace string
OverrideMeshGateway MeshGatewayConfig
OverrideProtocol string
OverrideConnectTimeout time.Duration
}{
connect: reconcile how upstream configuration works with discovery chains (#6225) * connect: reconcile how upstream configuration works with discovery chains The following upstream config fields for connect sidecars sanely integrate into discovery chain resolution: - Destination Namespace/Datacenter: Compilation occurs locally but using different default values for namespaces and datacenters. The xDS clusters that are created are named as they normally would be. - Mesh Gateway Mode (single upstream): If set this value overrides any value computed for any resolver for the entire discovery chain. The xDS clusters that are created may be named differently (see below). - Mesh Gateway Mode (whole sidecar): If set this value overrides any value computed for any resolver for the entire discovery chain. If this is specifically overridden for a single upstream this value is ignored in that case. The xDS clusters that are created may be named differently (see below). - Protocol (in opaque config): If set this value overrides the value computed when evaluating the entire discovery chain. If the normal chain would be TCP or if this override is set to TCP then the result is that we explicitly disable L7 Routing and Splitting. The xDS clusters that are created may be named differently (see below). - Connect Timeout (in opaque config): If set this value overrides the value for any resolver in the entire discovery chain. The xDS clusters that are created may be named differently (see below). If any of the above overrides affect the actual result of compiling the discovery chain (i.e. "tcp" becomes "grpc" instead of being a no-op override to "tcp") then the relevant parameters are hashed and provided to the xDS layer as a prefix for use in naming the Clusters. This is to ensure that if one Upstream discovery chain has no overrides and tangentially needs a cluster named "api.default.XXX", and another Upstream does have overrides for "api.default.XXX" that they won't cross-pollinate against the operator's wishes. Fixes #6159
5 years ago
Name: r.Name,
EvaluateInDatacenter: r.EvaluateInDatacenter,
EvaluateInNamespace: r.EvaluateInNamespace,
OverrideMeshGateway: r.OverrideMeshGateway,
OverrideProtocol: r.OverrideProtocol,
OverrideConnectTimeout: r.OverrideConnectTimeout,
}, 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 {
Chain *CompiledDiscoveryChain
QueryMeta
}
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
}
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
}
func defaultIfEmpty(val, defaultVal string) string {
if val != "" {
return val
}
return defaultVal
}