consul/agent/structs/discovery_chain.go
R.B. Boyer 9138a97054
Fix bug in service-resolver redirects if the destination uses a default resolver. (#6122)
Also:
- add back an internal http endpoint to dump a compiled discovery chain for debugging purposes

Before the CompiledDiscoveryChain.IsDefault() method would test:

- is this chain just one resolver step?
- is that resolver step just the default?

But what I forgot to test:

- is that resolver step for the same service that the chain represents?

This last point is important because if you configured just one config
entry:

    kind = "service-resolver"
    name = "web"
    redirect {
      service = "other"
    }

and requested the chain for "web" you'd get back a **default** resolver
for "other".  In the xDS code the IsDefault() method is used to
determine if this chain is "empty". If it is then we use the
pre-discovery-chain logic that just uses data embedded in the Upstream
object (and still lets the escape hatches function).

In the example above that means certain parts of the xDS code were going
to try referencing a cluster named "web..." despite the other parts of
the xDS code maintaining clusters named "other...".
2019-07-12 12:21:25 -05:00

258 lines
7.3 KiB
Go

package structs
import (
"bytes"
"encoding"
"fmt"
"net/url"
"sort"
"strings"
"time"
)
// CompiledDiscoveryChain is the result from taking a set of related config
// entries for a single service's discovery chain and restructuring them into a
// form that is more usable for actual service discovery.
type CompiledDiscoveryChain struct {
ServiceName string
Namespace string // the namespace that the chain was compiled within
Datacenter string // the datacenter that the chain was compiled within
Protocol string // overall protocol shared by everything in the chain
// Node is the top node in the chain.
//
// If this is a router or splitter then in envoy this renders as an http
// route object.
//
// If this is a group resolver then in envoy this renders as a default
// wildcard http route object.
Node *DiscoveryGraphNode `json:",omitempty"`
// GroupResolverNodes respresents all unique service instance groups that
// need to be represented. For envoy these render as Clusters.
//
// Omitted from JSON because DiscoveryTarget is not a encoding.TextMarshaler.
GroupResolverNodes map[DiscoveryTarget]*DiscoveryGraphNode `json:"-"`
// TODO(rb): not sure if these two fields are actually necessary but I'll know when I get into xDS
Resolvers map[string]*ServiceResolverConfigEntry `json:",omitempty"`
Targets []DiscoveryTarget `json:",omitempty"`
}
// IsDefault returns true if the compiled chain represents no routing, no
// splitting, and only the default resolution. We have to be careful here to
// avoid returning "yep this is default" when the only resolver action being
// applied is redirection to another resolver that is default, so we double
// check the resolver matches the requested resolver.
func (c *CompiledDiscoveryChain) IsDefault() bool {
if c.Node == nil {
return true
}
return c.Node.Name == c.ServiceName &&
c.Node.Type == DiscoveryGraphNodeTypeGroupResolver &&
c.Node.GroupResolver.Default
}
const (
DiscoveryGraphNodeTypeRouter = "router"
DiscoveryGraphNodeTypeSplitter = "splitter"
DiscoveryGraphNodeTypeGroupResolver = "group-resolver"
)
// DiscoveryGraphNode is a single node of the compiled discovery chain.
type DiscoveryGraphNode struct {
Type string
Name string // default chain/service name at this spot
// fields for Type==router
Routes []*DiscoveryRoute `json:",omitempty"`
// fields for Type==splitter
Splits []*DiscoverySplit `json:",omitempty"`
// fields for Type==group-resolver
GroupResolver *DiscoveryGroupResolver `json:",omitempty"`
}
// compiled form of ServiceResolverConfigEntry but customized per non-failover target
type DiscoveryGroupResolver struct {
Definition *ServiceResolverConfigEntry `json:",omitempty"`
Default bool `json:",omitempty"`
ConnectTimeout time.Duration `json:",omitempty"`
MeshGateway MeshGatewayConfig `json:",omitempty"`
Target DiscoveryTarget `json:",omitempty"`
Failover *DiscoveryFailover `json:",omitempty"`
}
// compiled form of ServiceRoute
type DiscoveryRoute struct {
Definition *ServiceRoute `json:",omitempty"`
DestinationNode *DiscoveryGraphNode `json:",omitempty"`
}
// compiled form of ServiceSplit
type DiscoverySplit struct {
Weight float32 `json:",omitempty"`
Node *DiscoveryGraphNode `json:",omitempty"`
}
// compiled form of ServiceResolverFailover
// TODO(rb): figure out how to get mesh gateways in here
type DiscoveryFailover struct {
Definition *ServiceResolverFailover `json:",omitempty"`
Targets []DiscoveryTarget `json:",omitempty"`
}
// DiscoveryTarget represents all of the inputs necessary to use a resolver
// config entry to execute a catalog query to generate a list of service
// instances during discovery.
//
// This is a value type so it can be used as a map key.
type DiscoveryTarget struct {
Service string `json:",omitempty"`
ServiceSubset string `json:",omitempty"`
Namespace string `json:",omitempty"`
Datacenter string `json:",omitempty"`
}
func (t DiscoveryTarget) IsEmpty() bool {
return t.Service == "" && t.ServiceSubset == "" && t.Namespace == "" && t.Datacenter == ""
}
// CopyAndModify will duplicate the target and selectively modify it given the
// requested inputs.
func (t DiscoveryTarget) CopyAndModify(
service,
serviceSubset,
namespace,
datacenter string,
) DiscoveryTarget {
t2 := t // copy
if service != "" && service != t2.Service {
t2.Service = service
// Reset the chosen subset if we reference a service other than our own.
t2.ServiceSubset = ""
}
if serviceSubset != "" && serviceSubset != t2.ServiceSubset {
t2.ServiceSubset = serviceSubset
}
if namespace != "" && namespace != t2.Namespace {
t2.Namespace = namespace
}
if datacenter != "" && datacenter != t2.Datacenter {
t2.Datacenter = datacenter
}
return t2
}
var _ encoding.TextMarshaler = DiscoveryTarget{}
var _ encoding.TextUnmarshaler = (*DiscoveryTarget)(nil)
// MarshalText implements encoding.TextMarshaler.
//
// This should also not include any colons for embedding that happens
// elsewhere.
//
// This should NOT return any errors.
func (t DiscoveryTarget) MarshalText() (text []byte, err error) {
var buf bytes.Buffer
buf.WriteString(url.QueryEscape(t.Service))
buf.WriteRune(',')
buf.WriteString(url.QueryEscape(t.ServiceSubset))
buf.WriteRune(',')
if t.Namespace != "default" {
buf.WriteString(url.QueryEscape(t.Namespace))
}
buf.WriteRune(',')
buf.WriteString(url.QueryEscape(t.Datacenter))
return buf.Bytes(), nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (t *DiscoveryTarget) UnmarshalText(text []byte) error {
parts := bytes.Split(text, []byte(","))
bad := false
if len(parts) != 4 {
return fmt.Errorf("invalid target: %q", string(text))
}
var err error
t.Service, err = url.QueryUnescape(string(parts[0]))
if err != nil {
bad = true
}
t.ServiceSubset, err = url.QueryUnescape(string(parts[1]))
if err != nil {
bad = true
}
t.Namespace, err = url.QueryUnescape(string(parts[2]))
if err != nil {
bad = true
}
t.Datacenter, err = url.QueryUnescape(string(parts[3]))
if err != nil {
bad = true
}
if bad {
return fmt.Errorf("invalid target: %q", string(text))
}
if t.Namespace == "" {
t.Namespace = "default"
}
return nil
}
func (t DiscoveryTarget) String() string {
var b strings.Builder
if t.ServiceSubset != "" {
b.WriteString(t.ServiceSubset)
} else {
b.WriteString("<default>")
}
b.WriteRune('.')
b.WriteString(t.Service)
b.WriteRune('.')
if t.Namespace != "" {
b.WriteString(t.Namespace)
} else {
b.WriteString("<default>")
}
b.WriteRune('.')
b.WriteString(t.Datacenter)
return b.String()
}
type DiscoveryTargets []DiscoveryTarget
func (targets DiscoveryTargets) Sort() {
sort.Slice(targets, func(i, j int) bool {
if targets[i].Service < targets[j].Service {
return true
} else if targets[i].Service > targets[j].Service {
return false
}
if targets[i].ServiceSubset < targets[j].ServiceSubset {
return true
} else if targets[i].ServiceSubset > targets[j].ServiceSubset {
return false
}
if targets[i].Namespace < targets[j].Namespace {
return true
} else if targets[i].Namespace > targets[j].Namespace {
return false
}
return targets[i].Datacenter < targets[j].Datacenter
})
}