mirror of
https://github.com/status-im/consul.git
synced 2025-02-19 09:07:59 +00:00
Follow up to https://github.com/hashicorp/consul/pull/10737#discussion_r682147950 Renames all variables for acl.Authorizer to use `authz`. Previously some places used `rule` which I believe was an old name carried over from the legacy ACL system. A couple places also used authorizer. This commit also removes another couple of authorizer nil checks that are no longer necessary.
1463 lines
43 KiB
Go
1463 lines
43 KiB
Go
package structs
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mitchellh/hashstructure"
|
|
|
|
"github.com/hashicorp/consul/acl"
|
|
"github.com/hashicorp/consul/agent/cache"
|
|
"github.com/hashicorp/consul/lib"
|
|
)
|
|
|
|
const (
|
|
// Names of Envoy's LB policies
|
|
LBPolicyMaglev = "maglev"
|
|
LBPolicyRingHash = "ring_hash"
|
|
LBPolicyRandom = "random"
|
|
LBPolicyLeastRequest = "least_request"
|
|
LBPolicyRoundRobin = "round_robin"
|
|
|
|
// Names of Envoy's LB policies
|
|
HashPolicyCookie = "cookie"
|
|
HashPolicyHeader = "header"
|
|
HashPolicyQueryParam = "query_parameter"
|
|
)
|
|
|
|
var (
|
|
validLBPolicies = map[string]bool{
|
|
"": true,
|
|
LBPolicyRandom: true,
|
|
LBPolicyRoundRobin: true,
|
|
LBPolicyLeastRequest: true,
|
|
LBPolicyRingHash: true,
|
|
LBPolicyMaglev: true,
|
|
}
|
|
|
|
validHashPolicies = map[string]bool{
|
|
HashPolicyHeader: true,
|
|
HashPolicyCookie: true,
|
|
HashPolicyQueryParam: true,
|
|
}
|
|
)
|
|
|
|
// 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
|
|
|
|
Meta map[string]string `json:",omitempty"`
|
|
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) GetMeta() map[string]string {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.Meta
|
|
}
|
|
|
|
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
|
|
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.NamespaceOrEmpty()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *ServiceRouterConfigEntry) Validate() error {
|
|
if e.Name == "" {
|
|
return fmt.Errorf("Name is required")
|
|
}
|
|
|
|
if err := validateConfigEntryMeta(e.Meta); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 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 !isValidHTTPMethod(m) {
|
|
return fmt.Errorf("Route[%d] Methods contains an invalid method %q", i, m)
|
|
}
|
|
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 isValidHTTPMethod(method string) bool {
|
|
switch method {
|
|
case http.MethodGet,
|
|
http.MethodHead,
|
|
http.MethodPost,
|
|
http.MethodPut,
|
|
http.MethodPatch,
|
|
http.MethodDelete,
|
|
http.MethodConnect,
|
|
http.MethodOptions,
|
|
http.MethodTrace:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (e *ServiceRouterConfigEntry) CanRead(authz acl.Authorizer) bool {
|
|
return canReadDiscoveryChain(e, authz)
|
|
}
|
|
|
|
func (e *ServiceRouterConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
|
return canWriteDiscoveryChain(e, authz)
|
|
}
|
|
|
|
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" alias:"path_exact"`
|
|
PathPrefix string `json:",omitempty" alias:"path_prefix"`
|
|
PathRegex string `json:",omitempty" alias:"path_regex"`
|
|
|
|
Header []ServiceRouteHTTPMatchHeader `json:",omitempty"`
|
|
QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty" alias:"query_param"`
|
|
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" alias:"service_subset"`
|
|
|
|
// 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"`
|
|
|
|
// NOTE: Partition is not represented here by design. Do not add it.
|
|
|
|
// 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" alias:"prefix_rewrite"`
|
|
|
|
// RequestTimeout is the total amount of time permitted for the entire
|
|
// downstream request (and retries) to be processed.
|
|
RequestTimeout time.Duration `json:",omitempty" alias:"request_timeout"`
|
|
|
|
// NumRetries is the number of times to retry the request when a retryable
|
|
// result occurs. This seems fairly proxy agnostic.
|
|
NumRetries uint32 `json:",omitempty" alias:"num_retries"`
|
|
|
|
// 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" alias:"retry_on_connect_failure"`
|
|
|
|
// RetryOnStatusCodes is a flat list of http response status codes that are
|
|
// eligible for retry. This again should be feasible in any reasonable proxy.
|
|
RetryOnStatusCodes []uint32 `json:",omitempty" alias:"retry_on_status_codes"`
|
|
}
|
|
|
|
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
|
|
|
|
Meta map[string]string `json:",omitempty"`
|
|
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) GetMeta() map[string]string {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.Meta
|
|
}
|
|
|
|
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(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")
|
|
}
|
|
|
|
if err := validateConfigEntryMeta(e.Meta); err != nil {
|
|
return err
|
|
}
|
|
|
|
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(authz acl.Authorizer) bool {
|
|
return canReadDiscoveryChain(e, authz)
|
|
}
|
|
|
|
func (e *ServiceSplitterConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
|
return canWriteDiscoveryChain(e, authz)
|
|
}
|
|
|
|
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" alias:"service_subset"`
|
|
|
|
// 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"`
|
|
|
|
// NOTE: Partition is not represented here by design. Do not add it.
|
|
}
|
|
|
|
// 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" alias:"default_subset"`
|
|
|
|
// 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" alias:"connect_timeout"`
|
|
|
|
// LoadBalancer determines the load balancing policy and configuration for services
|
|
// issuing requests to this upstream service.
|
|
LoadBalancer *LoadBalancer `json:",omitempty" alias:"load_balancer"`
|
|
|
|
Meta map[string]string `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 &&
|
|
e.LoadBalancer == nil
|
|
}
|
|
|
|
func (e *ServiceResolverConfigEntry) GetKind() string {
|
|
return ServiceResolver
|
|
}
|
|
|
|
func (e *ServiceResolverConfigEntry) GetName() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
|
|
return e.Name
|
|
}
|
|
|
|
func (e *ServiceResolverConfigEntry) GetMeta() map[string]string {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.Meta
|
|
}
|
|
|
|
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 err := validateConfigEntryMeta(e.Meta); err != nil {
|
|
return err
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if e.LoadBalancer != nil {
|
|
lb := e.LoadBalancer
|
|
|
|
if ok := validLBPolicies[lb.Policy]; !ok {
|
|
return fmt.Errorf("Bad LoadBalancer policy: %q is not supported", lb.Policy)
|
|
}
|
|
|
|
if lb.Policy != LBPolicyRingHash && lb.RingHashConfig != nil {
|
|
return fmt.Errorf("Bad LoadBalancer configuration. "+
|
|
"RingHashConfig specified for incompatible load balancing policy %q", lb.Policy)
|
|
}
|
|
if lb.Policy != LBPolicyLeastRequest && lb.LeastRequestConfig != nil {
|
|
return fmt.Errorf("Bad LoadBalancer configuration. "+
|
|
"LeastRequestConfig specified for incompatible load balancing policy %q", lb.Policy)
|
|
}
|
|
if !lb.IsHashBased() && len(lb.HashPolicies) > 0 {
|
|
return fmt.Errorf("Bad LoadBalancer configuration: "+
|
|
"HashPolicies specified for non-hash-based Policy: %q", lb.Policy)
|
|
}
|
|
|
|
for i, hp := range lb.HashPolicies {
|
|
if ok := validHashPolicies[hp.Field]; hp.Field != "" && !ok {
|
|
return fmt.Errorf("Bad LoadBalancer HashPolicy[%d]: %q is not a supported field", i, hp.Field)
|
|
}
|
|
|
|
if hp.SourceIP && hp.Field != "" {
|
|
return fmt.Errorf("Bad LoadBalancer HashPolicy[%d]: "+
|
|
"A single hash policy cannot hash both a source address and a %q", i, hp.Field)
|
|
}
|
|
if hp.SourceIP && hp.FieldValue != "" {
|
|
return fmt.Errorf("Bad LoadBalancer HashPolicy[%d]: "+
|
|
"A FieldValue cannot be specified when hashing SourceIP", i)
|
|
}
|
|
if hp.Field != "" && hp.FieldValue == "" {
|
|
return fmt.Errorf("Bad LoadBalancer HashPolicy[%d]: Field %q was specified without a FieldValue", i, hp.Field)
|
|
}
|
|
if hp.FieldValue != "" && hp.Field == "" {
|
|
return fmt.Errorf("Bad LoadBalancer HashPolicy[%d]: FieldValue requires a Field to apply to", i)
|
|
}
|
|
if hp.CookieConfig != nil {
|
|
if hp.Field != HashPolicyCookie {
|
|
return fmt.Errorf("Bad LoadBalancer HashPolicy[%d]: cookie_config provided for %q", i, hp.Field)
|
|
}
|
|
if hp.CookieConfig.Session && hp.CookieConfig.TTL != 0*time.Second {
|
|
return fmt.Errorf("Bad LoadBalancer HashPolicy[%d]: a session cookie cannot have an associated TTL", i)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *ServiceResolverConfigEntry) CanRead(authz acl.Authorizer) bool {
|
|
return canReadDiscoveryChain(e, authz)
|
|
}
|
|
|
|
func (e *ServiceResolverConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
|
return canWriteDiscoveryChain(e, authz)
|
|
}
|
|
|
|
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" alias:"only_passing"`
|
|
}
|
|
|
|
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" alias:"service_subset"`
|
|
|
|
// 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"`
|
|
|
|
// NOTE: Partition is not represented here by design. Do not add it.
|
|
}
|
|
|
|
// 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" alias:"service_subset"`
|
|
|
|
// 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"`
|
|
|
|
// NOTE: Partition is not represented here by design. Do not add it.
|
|
}
|
|
|
|
// LoadBalancer determines the load balancing policy and configuration for services
|
|
// issuing requests to this upstream service.
|
|
type LoadBalancer struct {
|
|
// Policy is the load balancing policy used to select a host
|
|
Policy string `json:",omitempty"`
|
|
|
|
// RingHashConfig contains configuration for the "ring_hash" policy type
|
|
RingHashConfig *RingHashConfig `json:",omitempty" alias:"ring_hash_config"`
|
|
|
|
// LeastRequestConfig contains configuration for the "least_request" policy type
|
|
LeastRequestConfig *LeastRequestConfig `json:",omitempty" alias:"least_request_config"`
|
|
|
|
// HashPolicies is a list of hash policies to use for hashing load balancing algorithms.
|
|
// Hash policies are evaluated individually and combined such that identical lists
|
|
// result in the same hash.
|
|
// If no hash policies are present, or none are successfully evaluated,
|
|
// then a random backend host will be selected.
|
|
HashPolicies []HashPolicy `json:",omitempty" alias:"hash_policies"`
|
|
}
|
|
|
|
// RingHashConfig contains configuration for the "ring_hash" policy type
|
|
type RingHashConfig struct {
|
|
// MinimumRingSize determines the minimum number of entries in the hash ring
|
|
MinimumRingSize uint64 `json:",omitempty" alias:"minimum_ring_size"`
|
|
|
|
// MaximumRingSize determines the maximum number of entries in the hash ring
|
|
MaximumRingSize uint64 `json:",omitempty" alias:"maximum_ring_size"`
|
|
}
|
|
|
|
// LeastRequestConfig contains configuration for the "least_request" policy type
|
|
type LeastRequestConfig struct {
|
|
// ChoiceCount determines the number of random healthy hosts from which to select the one with the least requests.
|
|
ChoiceCount uint32 `json:",omitempty" alias:"choice_count"`
|
|
}
|
|
|
|
// HashPolicy defines which attributes will be hashed by hash-based LB algorithms
|
|
type HashPolicy struct {
|
|
// Field is the attribute type to hash on.
|
|
// Must be one of "header","cookie", or "query_parameter".
|
|
// Cannot be specified along with SourceIP.
|
|
Field string `json:",omitempty"`
|
|
|
|
// FieldValue is the value to hash.
|
|
// ie. header name, cookie name, URL query parameter name
|
|
// Cannot be specified along with SourceIP.
|
|
FieldValue string `json:",omitempty" alias:"field_value"`
|
|
|
|
// CookieConfig contains configuration for the "cookie" hash policy type.
|
|
CookieConfig *CookieConfig `json:",omitempty" alias:"cookie_config"`
|
|
|
|
// SourceIP determines whether the hash should be of the source IP rather than of a field and field value.
|
|
// Cannot be specified along with Field or FieldValue.
|
|
SourceIP bool `json:",omitempty" alias:"source_ip"`
|
|
|
|
// Terminal will short circuit the computation of the hash when multiple hash policies are present.
|
|
// If a hash is computed when a Terminal policy is evaluated,
|
|
// then that hash will be used and subsequent hash policies will be ignored.
|
|
Terminal bool `json:",omitempty"`
|
|
}
|
|
|
|
// CookieConfig contains configuration for the "cookie" hash policy type.
|
|
// This is specified to have Envoy generate a cookie for a client on its first request.
|
|
type CookieConfig struct {
|
|
// Generates a session cookie with no expiration.
|
|
Session bool `json:",omitempty"`
|
|
|
|
// TTL for generated cookies. Cannot be specified for session cookies.
|
|
TTL time.Duration `json:",omitempty"`
|
|
|
|
// The path to set for the cookie
|
|
Path string `json:",omitempty"`
|
|
}
|
|
|
|
func (lb *LoadBalancer) IsHashBased() bool {
|
|
if lb == nil {
|
|
return false
|
|
}
|
|
|
|
switch lb.Policy {
|
|
case LBPolicyMaglev, LBPolicyRingHash:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
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, authz acl.Authorizer) bool {
|
|
entryID := NewServiceID(entry.GetName(), entry.GetEnterpriseMeta())
|
|
|
|
var authzContext acl.AuthorizerContext
|
|
entryID.FillAuthzContext(&authzContext)
|
|
|
|
name := entry.GetName()
|
|
|
|
if authz.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 authz.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 {
|
|
Name string
|
|
EvaluateInDatacenter string
|
|
EvaluateInNamespace string
|
|
|
|
// NOTE: Partition is not represented here by design. Do not add it.
|
|
|
|
// 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 {
|
|
Name string
|
|
EvaluateInDatacenter string
|
|
EvaluateInNamespace string
|
|
OverrideMeshGateway MeshGatewayConfig
|
|
OverrideProtocol string
|
|
OverrideConnectTimeout time.Duration
|
|
Filter string
|
|
}{
|
|
Name: r.Name,
|
|
EvaluateInDatacenter: r.EvaluateInDatacenter,
|
|
EvaluateInNamespace: r.EvaluateInNamespace,
|
|
OverrideMeshGateway: r.OverrideMeshGateway,
|
|
OverrideProtocol: r.OverrideProtocol,
|
|
OverrideConnectTimeout: r.OverrideConnectTimeout,
|
|
Filter: r.QueryOptions.Filter,
|
|
}, 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
|
|
}
|
|
|
|
func IsProtocolHTTPLike(protocol string) bool {
|
|
switch protocol {
|
|
case "http", "http2", "grpc":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|