mirror of https://github.com/status-im/consul.git
do some initial config entry graph validation during writes (#6047)
This commit is contained in:
parent
43bda6fb76
commit
2ad516aeaf
|
@ -8,7 +8,13 @@ import (
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(rb): surface any specific errors that may matter during graph vetting at write-time (like mixing protocols)
|
type CompileRequest struct {
|
||||||
|
ServiceName string
|
||||||
|
CurrentNamespace string
|
||||||
|
CurrentDatacenter string
|
||||||
|
InferDefaults bool // TODO(rb): remove this?
|
||||||
|
Entries *structs.DiscoveryChainConfigEntries
|
||||||
|
}
|
||||||
|
|
||||||
// Compile assembles a discovery chain in the form of a graph of nodes using
|
// Compile assembles a discovery chain in the form of a graph of nodes using
|
||||||
// raw config entries and local context.
|
// raw config entries and local context.
|
||||||
|
@ -19,13 +25,19 @@ import (
|
||||||
// Omitting router and splitter entries for services not using an L7 protocol
|
// Omitting router and splitter entries for services not using an L7 protocol
|
||||||
// (like HTTP) happens during initial fetching, but for sanity purposes a quick
|
// (like HTTP) happens during initial fetching, but for sanity purposes a quick
|
||||||
// reinforcement of that happens here, too.
|
// reinforcement of that happens here, too.
|
||||||
func Compile(
|
//
|
||||||
serviceName string,
|
// May return a *structs.ConfigEntryGraphError, but that is only expected when
|
||||||
currentNamespace string,
|
// being used to validate modifications to the config entry graph. It should
|
||||||
currentDatacenter string,
|
// not be expected when compiling existing entries at runtime that are already
|
||||||
inferDefaults bool,
|
// valid.
|
||||||
entries *structs.DiscoveryChainConfigEntries,
|
func Compile(req CompileRequest) (*structs.CompiledDiscoveryChain, error) {
|
||||||
) (*structs.CompiledDiscoveryChain, error) {
|
var (
|
||||||
|
serviceName = req.ServiceName
|
||||||
|
currentNamespace = req.CurrentNamespace
|
||||||
|
currentDatacenter = req.CurrentDatacenter
|
||||||
|
inferDefaults = req.InferDefaults
|
||||||
|
entries = req.Entries
|
||||||
|
)
|
||||||
if serviceName == "" {
|
if serviceName == "" {
|
||||||
return nil, fmt.Errorf("serviceName is required")
|
return nil, fmt.Errorf("serviceName is required")
|
||||||
}
|
}
|
||||||
|
@ -39,10 +51,6 @@ func Compile(
|
||||||
return nil, fmt.Errorf("entries is required")
|
return nil, fmt.Errorf("entries is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// This shouldn't be necessary, but do it anyway. It is the one place input
|
|
||||||
// mutation will occur, but only if the caller forgot in the first place.
|
|
||||||
entries.Fixup()
|
|
||||||
|
|
||||||
c := &compiler{
|
c := &compiler{
|
||||||
serviceName: serviceName,
|
serviceName: serviceName,
|
||||||
currentNamespace: currentNamespace,
|
currentNamespace: currentNamespace,
|
||||||
|
@ -85,6 +93,10 @@ type compiler struct {
|
||||||
splitterNodes map[string]*structs.DiscoveryGraphNode
|
splitterNodes map[string]*structs.DiscoveryGraphNode
|
||||||
groupResolverNodes map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode // this is also an OUTPUT field
|
groupResolverNodes map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode // this is also an OUTPUT field
|
||||||
|
|
||||||
|
// usesAdvancedRoutingFeatures is set to true if config entries for routing
|
||||||
|
// or splitting appear in the compiled chain
|
||||||
|
usesAdvancedRoutingFeatures bool
|
||||||
|
|
||||||
// topNode is computed inside of assembleChain()
|
// topNode is computed inside of assembleChain()
|
||||||
//
|
//
|
||||||
// This is an OUTPUT field.
|
// This is an OUTPUT field.
|
||||||
|
@ -113,11 +125,10 @@ type compiler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compiler) recordServiceProtocol(serviceName string) error {
|
func (c *compiler) recordServiceProtocol(serviceName string) error {
|
||||||
if serviceDefault, ok := c.entries.Services[serviceName]; ok {
|
if serviceDefault := c.entries.GetService(serviceName); serviceDefault != nil {
|
||||||
return c.recordProtocol(serviceName, serviceDefault.Protocol)
|
return c.recordProtocol(serviceName, serviceDefault.Protocol)
|
||||||
} else {
|
|
||||||
return c.recordProtocol(serviceName, "")
|
|
||||||
}
|
}
|
||||||
|
return c.recordProtocol(serviceName, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compiler) recordProtocol(fromService, protocol string) error {
|
func (c *compiler) recordProtocol(fromService, protocol string) error {
|
||||||
|
@ -130,8 +141,12 @@ func (c *compiler) recordProtocol(fromService, protocol string) error {
|
||||||
if c.protocol == "" {
|
if c.protocol == "" {
|
||||||
c.protocol = protocol
|
c.protocol = protocol
|
||||||
} else if c.protocol != protocol {
|
} else if c.protocol != protocol {
|
||||||
// TODO(rb): avoid this during config entry writes instead
|
return &structs.ConfigEntryGraphError{
|
||||||
return fmt.Errorf("discovery chain %q uses inconsistent protocols; service %q has %q != %q", c.serviceName, fromService, protocol, c.protocol)
|
Message: fmt.Sprintf(
|
||||||
|
"discovery chain %q uses inconsistent protocols; service %q has %q which is not %q",
|
||||||
|
c.serviceName, fromService, protocol, c.protocol,
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -165,6 +180,15 @@ func (c *compiler) compile() (*structs.CompiledDiscoveryChain, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !enableAdvancedRoutingForProtocol(c.protocol) && c.usesAdvancedRoutingFeatures {
|
||||||
|
return nil, &structs.ConfigEntryGraphError{
|
||||||
|
Message: fmt.Sprintf(
|
||||||
|
"discovery chain %q uses a protocol %q that does not permit advanced routing or splitting behavior",
|
||||||
|
c.serviceName, c.protocol,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
targets := make([]structs.DiscoveryTarget, 0, len(c.targets))
|
targets := make([]structs.DiscoveryTarget, 0, len(c.targets))
|
||||||
for target, _ := range c.targets {
|
for target, _ := range c.targets {
|
||||||
targets = append(targets, target)
|
targets = append(targets, target)
|
||||||
|
@ -252,8 +276,8 @@ func (c *compiler) assembleChain() error {
|
||||||
|
|
||||||
// The only router we consult is the one for the service name at the top of
|
// The only router we consult is the one for the service name at the top of
|
||||||
// the chain.
|
// the chain.
|
||||||
router, ok := c.entries.Routers[c.serviceName]
|
router := c.entries.GetRouter(c.serviceName)
|
||||||
if !ok {
|
if router == nil {
|
||||||
// If no router is configured, move on down the line to the next hop of
|
// If no router is configured, move on down the line to the next hop of
|
||||||
// the chain.
|
// the chain.
|
||||||
node, err := c.getSplitterOrGroupResolverNode(c.newTarget(c.serviceName, "", "", ""))
|
node, err := c.getSplitterOrGroupResolverNode(c.newTarget(c.serviceName, "", "", ""))
|
||||||
|
@ -270,6 +294,7 @@ func (c *compiler) assembleChain() error {
|
||||||
Name: router.Name,
|
Name: router.Name,
|
||||||
Routes: make([]*structs.DiscoveryRoute, 0, len(router.Routes)+1),
|
Routes: make([]*structs.DiscoveryRoute, 0, len(router.Routes)+1),
|
||||||
}
|
}
|
||||||
|
c.usesAdvancedRoutingFeatures = true
|
||||||
if err := c.recordServiceProtocol(router.Name); err != nil {
|
if err := c.recordServiceProtocol(router.Name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -368,8 +393,8 @@ func (c *compiler) getSplitterNode(name string) (*structs.DiscoveryGraphNode, er
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the config entry.
|
// Fetch the config entry.
|
||||||
splitter, ok := c.entries.Splitters[name]
|
splitter := c.entries.GetSplitter(name)
|
||||||
if !ok {
|
if splitter == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -413,6 +438,7 @@ func (c *compiler) getSplitterNode(name string) (*structs.DiscoveryGraphNode, er
|
||||||
compiledSplit.Node = node
|
compiledSplit.Node = node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.usesAdvancedRoutingFeatures = true
|
||||||
return splitNode, nil
|
return splitNode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,6 +465,8 @@ RESOLVE_AGAIN:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle redirects right up front.
|
// Handle redirects right up front.
|
||||||
|
//
|
||||||
|
// TODO(rb): What about a redirected subset reference? (web/v2, but web redirects to alt/"")
|
||||||
if resolver.Redirect != nil {
|
if resolver.Redirect != nil {
|
||||||
redirect := resolver.Redirect
|
redirect := resolver.Redirect
|
||||||
|
|
||||||
|
@ -460,15 +488,18 @@ RESOLVE_AGAIN:
|
||||||
goto RESOLVE_AGAIN
|
goto RESOLVE_AGAIN
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since we're actually building a node with it, we can keep it.
|
if target.ServiceSubset != "" && !resolver.SubsetExists(target.ServiceSubset) {
|
||||||
//
|
return nil, &structs.ConfigEntryGraphError{
|
||||||
// TODO(rb): maybe infer this from the keyspace of the groupresolvernodes slice.
|
Message: fmt.Sprintf(
|
||||||
c.retainResolvers[target.Service] = struct{}{}
|
"service %q does not have a subset named %q",
|
||||||
|
target.Service,
|
||||||
if target.Service != resolver.Name {
|
target.ServiceSubset,
|
||||||
//TODO(rb): remove
|
),
|
||||||
panic("NOT POSSIBLE")
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we're actually building a node with it, we can keep it.
|
||||||
|
c.retainResolvers[target.Service] = struct{}{}
|
||||||
|
|
||||||
connectTimeout := resolver.ConnectTimeout
|
connectTimeout := resolver.ConnectTimeout
|
||||||
if connectTimeout < 1 {
|
if connectTimeout < 1 {
|
||||||
|
@ -555,9 +586,6 @@ RESOLVE_AGAIN:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if failoverGroupResolverNode.Type != structs.DiscoveryGraphNodeTypeGroupResolver {
|
|
||||||
panic("TODO(rb)(remove): '" + failoverGroupResolverNode.Type + "' is not a group-resolver node")
|
|
||||||
}
|
|
||||||
failoverTarget := failoverGroupResolverNode.GroupResolver.Target
|
failoverTarget := failoverGroupResolverNode.GroupResolver.Target
|
||||||
df.Targets = append(df.Targets, failoverTarget)
|
df.Targets = append(df.Targets, failoverTarget)
|
||||||
}
|
}
|
||||||
|
@ -581,3 +609,12 @@ func defaultIfEmpty(val, defaultVal string) string {
|
||||||
}
|
}
|
||||||
return defaultVal
|
return defaultVal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func enableAdvancedRoutingForProtocol(protocol string) bool {
|
||||||
|
switch protocol {
|
||||||
|
case "http", "http2", "grpc":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,13 @@ import (
|
||||||
func TestCompile_NoEntries_NoInferDefaults(t *testing.T) {
|
func TestCompile_NoEntries_NoInferDefaults(t *testing.T) {
|
||||||
entries := newEntries()
|
entries := newEntries()
|
||||||
|
|
||||||
res, err := Compile("main", "default", "dc1", false, entries)
|
res, err := Compile(CompileRequest{
|
||||||
|
ServiceName: "main",
|
||||||
|
CurrentNamespace: "default",
|
||||||
|
CurrentDatacenter: "dc1",
|
||||||
|
InferDefaults: false,
|
||||||
|
Entries: entries,
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Nil(t, res)
|
require.Nil(t, res)
|
||||||
}
|
}
|
||||||
|
@ -19,6 +25,8 @@ func TestCompile_NoEntries_NoInferDefaults(t *testing.T) {
|
||||||
type compileTestCase struct {
|
type compileTestCase struct {
|
||||||
entries *structs.DiscoveryChainConfigEntries
|
entries *structs.DiscoveryChainConfigEntries
|
||||||
expect *structs.CompiledDiscoveryChain // the GroupResolverNodes map should have nil values
|
expect *structs.CompiledDiscoveryChain // the GroupResolverNodes map should have nil values
|
||||||
|
expectErr string
|
||||||
|
expectGraphErr bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCompile(t *testing.T) {
|
func TestCompile(t *testing.T) {
|
||||||
|
@ -45,9 +53,19 @@ func TestCompile(t *testing.T) {
|
||||||
"noop split to resolver with default subset": testcase_NoopSplit_WithDefaultSubset(),
|
"noop split to resolver with default subset": testcase_NoopSplit_WithDefaultSubset(),
|
||||||
"resolver with default subset": testcase_Resolve_WithDefaultSubset(),
|
"resolver with default subset": testcase_Resolve_WithDefaultSubset(),
|
||||||
"resolver with no entries and inferring defaults": testcase_DefaultResolver(),
|
"resolver with no entries and inferring defaults": testcase_DefaultResolver(),
|
||||||
|
|
||||||
// TODO(rb): handle this case better: "circular split": testcase_CircularSplit(),
|
// TODO(rb): handle this case better: "circular split": testcase_CircularSplit(),
|
||||||
"all the bells and whistles": testcase_AllBellsAndWhistles(),
|
"all the bells and whistles": testcase_AllBellsAndWhistles(),
|
||||||
"multi dc canary": testcase_MultiDatacenterCanary(),
|
"multi dc canary": testcase_MultiDatacenterCanary(),
|
||||||
|
|
||||||
|
// various errors
|
||||||
|
"splitter requires valid protocol": testcase_SplitterRequiresValidProtocol(),
|
||||||
|
"router requires valid protocol": testcase_RouterRequiresValidProtocol(),
|
||||||
|
"split to unsplittable protocol": testcase_SplitToUnsplittableProtocol(),
|
||||||
|
"route to unroutable protocol": testcase_RouteToUnroutableProtocol(),
|
||||||
|
"failover crosses protocols": testcase_FailoverCrossesProtocols(),
|
||||||
|
"redirect crosses protocols": testcase_RedirectCrossesProtocols(),
|
||||||
|
"redirect to missing subset": testcase_RedirectToMissingSubset(),
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, tc := range cases {
|
for name, tc := range cases {
|
||||||
|
@ -69,7 +87,23 @@ func TestCompile(t *testing.T) {
|
||||||
require.NoError(t, entry.Validate())
|
require.NoError(t, entry.Validate())
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := Compile("main", "default", "dc1", true, tc.entries)
|
res, err := Compile(CompileRequest{
|
||||||
|
ServiceName: "main",
|
||||||
|
CurrentNamespace: "default",
|
||||||
|
CurrentDatacenter: "dc1",
|
||||||
|
InferDefaults: true,
|
||||||
|
Entries: tc.entries,
|
||||||
|
})
|
||||||
|
if tc.expectErr != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.expectErr)
|
||||||
|
_, ok := err.(*structs.ConfigEntryGraphError)
|
||||||
|
if tc.expectGraphErr {
|
||||||
|
require.True(t, ok, "%T is not a *ConfigEntryGraphError", err)
|
||||||
|
} else {
|
||||||
|
require.False(t, ok, "did not expect a *ConfigEntryGraphError here: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Avoid requiring unnecessary test boilerplate and inject these
|
// Avoid requiring unnecessary test boilerplate and inject these
|
||||||
|
@ -89,6 +123,7 @@ func TestCompile(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
require.Equal(t, tc.expect, res)
|
require.Equal(t, tc.expect, res)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -365,7 +400,7 @@ func testcase_RouteBypassesSplit() compileTestCase {
|
||||||
Name: "other",
|
Name: "other",
|
||||||
Subsets: map[string]structs.ServiceResolverSubset{
|
Subsets: map[string]structs.ServiceResolverSubset{
|
||||||
"bypass": {
|
"bypass": {
|
||||||
Filter: "ServiceMeta.version == bypass",
|
Filter: "Service.Meta.version == bypass",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -554,10 +589,10 @@ func testcase_SubsetSplit() compileTestCase {
|
||||||
Name: "main",
|
Name: "main",
|
||||||
Subsets: map[string]structs.ServiceResolverSubset{
|
Subsets: map[string]structs.ServiceResolverSubset{
|
||||||
"v1": {
|
"v1": {
|
||||||
Filter: "ServiceMeta.version == 1",
|
Filter: "Service.Meta.version == 1",
|
||||||
},
|
},
|
||||||
"v2": {
|
"v2": {
|
||||||
Filter: "ServiceMeta.version == 2",
|
Filter: "Service.Meta.version == 2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -718,10 +753,10 @@ func testcase_SplitBypassesSplit() compileTestCase {
|
||||||
Name: "next",
|
Name: "next",
|
||||||
Subsets: map[string]structs.ServiceResolverSubset{
|
Subsets: map[string]structs.ServiceResolverSubset{
|
||||||
"bypassed": {
|
"bypassed": {
|
||||||
Filter: "ServiceMeta.version == bypass",
|
Filter: "Service.Meta.version == bypass",
|
||||||
},
|
},
|
||||||
"not-bypassed": {
|
"not-bypassed": {
|
||||||
Filter: "ServiceMeta.version != bypass",
|
Filter: "Service.Meta.version != bypass",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -819,10 +854,10 @@ func testcase_ServiceAndSubsetRedirect() compileTestCase {
|
||||||
Name: "other",
|
Name: "other",
|
||||||
Subsets: map[string]structs.ServiceResolverSubset{
|
Subsets: map[string]structs.ServiceResolverSubset{
|
||||||
"v1": {
|
"v1": {
|
||||||
Filter: "ServiceMeta.version == 1",
|
Filter: "Service.Meta.version == 1",
|
||||||
},
|
},
|
||||||
"v2": {
|
"v2": {
|
||||||
Filter: "ServiceMeta.version == 2",
|
Filter: "Service.Meta.version == 2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -949,7 +984,7 @@ func testcase_ServiceAndSubsetFailover() compileTestCase {
|
||||||
Name: "main",
|
Name: "main",
|
||||||
Subsets: map[string]structs.ServiceResolverSubset{
|
Subsets: map[string]structs.ServiceResolverSubset{
|
||||||
"backup": {
|
"backup": {
|
||||||
Filter: "ServiceMeta.version == backup",
|
Filter: "Service.Meta.version == backup",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Failover: map[string]structs.ServiceResolverFailover{
|
Failover: map[string]structs.ServiceResolverFailover{
|
||||||
|
@ -1059,8 +1094,8 @@ func testcase_NoopSplit_WithDefaultSubset() compileTestCase {
|
||||||
Name: "main",
|
Name: "main",
|
||||||
DefaultSubset: "v2",
|
DefaultSubset: "v2",
|
||||||
Subsets: map[string]structs.ServiceResolverSubset{
|
Subsets: map[string]structs.ServiceResolverSubset{
|
||||||
"v1": {Filter: "ServiceMeta.version == 1"},
|
"v1": {Filter: "Service.Meta.version == 1"},
|
||||||
"v2": {Filter: "ServiceMeta.version == 2"},
|
"v2": {Filter: "Service.Meta.version == 2"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1138,8 +1173,8 @@ func testcase_Resolve_WithDefaultSubset() compileTestCase {
|
||||||
Name: "main",
|
Name: "main",
|
||||||
DefaultSubset: "v2",
|
DefaultSubset: "v2",
|
||||||
Subsets: map[string]structs.ServiceResolverSubset{
|
Subsets: map[string]structs.ServiceResolverSubset{
|
||||||
"v1": {Filter: "ServiceMeta.version == 1"},
|
"v1": {Filter: "Service.Meta.version == 1"},
|
||||||
"v2": {Filter: "ServiceMeta.version == 2"},
|
"v2": {Filter: "Service.Meta.version == 2"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1394,9 +1429,9 @@ func testcase_AllBellsAndWhistles() compileTestCase {
|
||||||
Name: "main",
|
Name: "main",
|
||||||
DefaultSubset: "default-subset",
|
DefaultSubset: "default-subset",
|
||||||
Subsets: map[string]structs.ServiceResolverSubset{
|
Subsets: map[string]structs.ServiceResolverSubset{
|
||||||
"v1": {Filter: "ServiceMeta.version == 1"},
|
"v1": {Filter: "Service.Meta.version == 1"},
|
||||||
"v2": {Filter: "ServiceMeta.version == 2"},
|
"v2": {Filter: "Service.Meta.version == 2"},
|
||||||
"v3": {Filter: "ServiceMeta.version == 3"},
|
"v3": {Filter: "Service.Meta.version == 3"},
|
||||||
"default-subset": {OnlyPassing: true},
|
"default-subset": {OnlyPassing: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1519,6 +1554,181 @@ func testcase_AllBellsAndWhistles() compileTestCase {
|
||||||
return compileTestCase{entries: entries, expect: expect}
|
return compileTestCase{entries: entries, expect: expect}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testcase_SplitterRequiresValidProtocol() compileTestCase {
|
||||||
|
entries := newEntries()
|
||||||
|
setServiceProtocol(entries, "main", "tcp")
|
||||||
|
|
||||||
|
entries.AddSplitters(
|
||||||
|
&structs.ServiceSplitterConfigEntry{
|
||||||
|
Kind: structs.ServiceSplitter,
|
||||||
|
Name: "main",
|
||||||
|
Splits: []structs.ServiceSplit{
|
||||||
|
{Weight: 90, Namespace: "v1"},
|
||||||
|
{Weight: 10, Namespace: "v2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return compileTestCase{
|
||||||
|
entries: entries,
|
||||||
|
expectErr: "does not permit advanced routing or splitting behavior",
|
||||||
|
expectGraphErr: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testcase_RouterRequiresValidProtocol() compileTestCase {
|
||||||
|
entries := newEntries()
|
||||||
|
setServiceProtocol(entries, "main", "tcp")
|
||||||
|
|
||||||
|
entries.AddRouters(
|
||||||
|
&structs.ServiceRouterConfigEntry{
|
||||||
|
Kind: structs.ServiceRouter,
|
||||||
|
Name: "main",
|
||||||
|
Routes: []structs.ServiceRoute{
|
||||||
|
{
|
||||||
|
Match: &structs.ServiceRouteMatch{
|
||||||
|
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "/other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &structs.ServiceRouteDestination{
|
||||||
|
Namespace: "other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return compileTestCase{
|
||||||
|
entries: entries,
|
||||||
|
expectErr: "does not permit advanced routing or splitting behavior",
|
||||||
|
expectGraphErr: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testcase_SplitToUnsplittableProtocol() compileTestCase {
|
||||||
|
entries := newEntries()
|
||||||
|
setServiceProtocol(entries, "main", "tcp")
|
||||||
|
setServiceProtocol(entries, "other", "tcp")
|
||||||
|
|
||||||
|
entries.AddSplitters(
|
||||||
|
&structs.ServiceSplitterConfigEntry{
|
||||||
|
Kind: structs.ServiceSplitter,
|
||||||
|
Name: "main",
|
||||||
|
Splits: []structs.ServiceSplit{
|
||||||
|
{Weight: 90},
|
||||||
|
{Weight: 10, Service: "other"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return compileTestCase{
|
||||||
|
entries: entries,
|
||||||
|
expectErr: "does not permit advanced routing or splitting behavior",
|
||||||
|
expectGraphErr: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testcase_RouteToUnroutableProtocol() compileTestCase {
|
||||||
|
entries := newEntries()
|
||||||
|
setServiceProtocol(entries, "main", "tcp")
|
||||||
|
setServiceProtocol(entries, "other", "tcp")
|
||||||
|
|
||||||
|
entries.AddRouters(
|
||||||
|
&structs.ServiceRouterConfigEntry{
|
||||||
|
Kind: structs.ServiceRouter,
|
||||||
|
Name: "main",
|
||||||
|
Routes: []structs.ServiceRoute{
|
||||||
|
{
|
||||||
|
Match: &structs.ServiceRouteMatch{
|
||||||
|
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "/other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &structs.ServiceRouteDestination{
|
||||||
|
Service: "other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return compileTestCase{
|
||||||
|
entries: entries,
|
||||||
|
expectErr: "does not permit advanced routing or splitting behavior",
|
||||||
|
expectGraphErr: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testcase_FailoverCrossesProtocols() compileTestCase {
|
||||||
|
entries := newEntries()
|
||||||
|
setServiceProtocol(entries, "main", "grpc")
|
||||||
|
setServiceProtocol(entries, "other", "tcp")
|
||||||
|
|
||||||
|
entries.AddResolvers(
|
||||||
|
&structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "main",
|
||||||
|
Failover: map[string]structs.ServiceResolverFailover{
|
||||||
|
"*": structs.ServiceResolverFailover{
|
||||||
|
Service: "other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return compileTestCase{
|
||||||
|
entries: entries,
|
||||||
|
expectErr: "uses inconsistent protocols",
|
||||||
|
expectGraphErr: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testcase_RedirectCrossesProtocols() compileTestCase {
|
||||||
|
entries := newEntries()
|
||||||
|
setServiceProtocol(entries, "main", "grpc")
|
||||||
|
setServiceProtocol(entries, "other", "tcp")
|
||||||
|
|
||||||
|
entries.AddResolvers(
|
||||||
|
&structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "main",
|
||||||
|
Redirect: &structs.ServiceResolverRedirect{
|
||||||
|
Service: "other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return compileTestCase{
|
||||||
|
entries: entries,
|
||||||
|
expectErr: "uses inconsistent protocols",
|
||||||
|
expectGraphErr: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testcase_RedirectToMissingSubset() compileTestCase {
|
||||||
|
entries := newEntries()
|
||||||
|
|
||||||
|
entries.AddResolvers(
|
||||||
|
&structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "other",
|
||||||
|
ConnectTimeout: 33 * time.Second,
|
||||||
|
},
|
||||||
|
&structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "main",
|
||||||
|
Redirect: &structs.ServiceResolverRedirect{
|
||||||
|
Service: "other",
|
||||||
|
ServiceSubset: "v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return compileTestCase{
|
||||||
|
entries: entries,
|
||||||
|
expectErr: `does not have a subset named "v1"`,
|
||||||
|
expectGraphErr: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newSimpleRoute(name string, muts ...func(*structs.ServiceRoute)) structs.ServiceRoute {
|
func newSimpleRoute(name string, muts ...func(*structs.ServiceRoute)) structs.ServiceRoute {
|
||||||
r := structs.ServiceRoute{
|
r := structs.ServiceRoute{
|
||||||
Match: &structs.ServiceRouteMatch{
|
Match: &structs.ServiceRouteMatch{
|
||||||
|
|
|
@ -3,6 +3,7 @@ package state
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/consul/discoverychain"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
memdb "github.com/hashicorp/go-memdb"
|
memdb "github.com/hashicorp/go-memdb"
|
||||||
)
|
)
|
||||||
|
@ -43,10 +44,79 @@ func configTableSchema() *memdb.TableSchema {
|
||||||
Lowercase: true,
|
Lowercase: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"link": &memdb.IndexSchema{
|
||||||
|
Name: "link",
|
||||||
|
AllowMissing: true,
|
||||||
|
Unique: false,
|
||||||
|
Indexer: &ConfigEntryLinkIndex{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConfigEntryLinkIndex struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type discoveryChainConfigEntry interface {
|
||||||
|
structs.ConfigEntry
|
||||||
|
// ListRelatedServices returns a list of other names of services referenced
|
||||||
|
// in this config entry.
|
||||||
|
ListRelatedServices() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigEntryLinkIndex) FromObject(obj interface{}) (bool, [][]byte, error) {
|
||||||
|
entry, ok := obj.(structs.ConfigEntry)
|
||||||
|
if !ok {
|
||||||
|
return false, nil, fmt.Errorf("object is not a ConfigEntry")
|
||||||
|
}
|
||||||
|
|
||||||
|
dcEntry, ok := entry.(discoveryChainConfigEntry)
|
||||||
|
if !ok {
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
linkedServices := dcEntry.ListRelatedServices()
|
||||||
|
|
||||||
|
numLinks := len(linkedServices)
|
||||||
|
if numLinks == 0 {
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
vals := make([][]byte, 0, numLinks)
|
||||||
|
for _, linkedService := range linkedServices {
|
||||||
|
vals = append(vals, []byte(linkedService+"\x00"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, vals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigEntryLinkIndex) FromArgs(args ...interface{}) ([]byte, error) {
|
||||||
|
if len(args) != 1 {
|
||||||
|
return nil, fmt.Errorf("must provide only a single argument")
|
||||||
|
}
|
||||||
|
arg, ok := args[0].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("argument must be a string: %#v", args[0])
|
||||||
|
}
|
||||||
|
// Add the null character as a terminator
|
||||||
|
arg += "\x00"
|
||||||
|
return []byte(arg), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigEntryLinkIndex) PrefixFromArgs(args ...interface{}) ([]byte, error) {
|
||||||
|
val, err := s.FromArgs(args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip the null terminator, the rest is a prefix
|
||||||
|
n := len(val)
|
||||||
|
if n > 0 {
|
||||||
|
return val[:n-1], nil
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
registerSchema(configTableSchema)
|
registerSchema(configTableSchema)
|
||||||
}
|
}
|
||||||
|
@ -173,6 +243,23 @@ func (s *Store) ensureConfigEntryTxn(tx *memdb.Txn, idx uint64, conf structs.Con
|
||||||
}
|
}
|
||||||
raftIndex.ModifyIndex = idx
|
raftIndex.ModifyIndex = idx
|
||||||
|
|
||||||
|
var existingConf structs.ConfigEntry
|
||||||
|
if existing != nil {
|
||||||
|
existingConf = existing.(structs.ConfigEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.validateProposedConfigEntryInGraph(
|
||||||
|
tx,
|
||||||
|
idx,
|
||||||
|
conf.GetKind(),
|
||||||
|
conf.GetName(),
|
||||||
|
existingConf,
|
||||||
|
conf,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err // Err is already sufficiently decorated.
|
||||||
|
}
|
||||||
|
|
||||||
// Insert the config entry and update the index
|
// Insert the config entry and update the index
|
||||||
if err := tx.Insert(configTableName, conf); err != nil {
|
if err := tx.Insert(configTableName, conf); err != nil {
|
||||||
return fmt.Errorf("failed inserting config entry: %s", err)
|
return fmt.Errorf("failed inserting config entry: %s", err)
|
||||||
|
@ -232,6 +319,23 @@ func (s *Store) DeleteConfigEntry(idx uint64, kind, name string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var existingConf structs.ConfigEntry
|
||||||
|
if existing != nil {
|
||||||
|
existingConf = existing.(structs.ConfigEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.validateProposedConfigEntryInGraph(
|
||||||
|
tx,
|
||||||
|
idx,
|
||||||
|
kind,
|
||||||
|
name,
|
||||||
|
existingConf,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err // Err is already sufficiently decorated.
|
||||||
|
}
|
||||||
|
|
||||||
// Delete the config entry from the DB and update the index.
|
// Delete the config entry from the DB and update the index.
|
||||||
if err := tx.Delete(configTableName, existing); err != nil {
|
if err := tx.Delete(configTableName, existing); err != nil {
|
||||||
return fmt.Errorf("failed removing check: %s", err)
|
return fmt.Errorf("failed removing check: %s", err)
|
||||||
|
@ -243,3 +347,414 @@ func (s *Store) DeleteConfigEntry(idx uint64, kind, name string) error {
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateProposedConfigEntryInGraph can be used to verify graph integrity for
|
||||||
|
// a proposed graph create/update/delete.
|
||||||
|
//
|
||||||
|
// This must be called before any mutations occur on the config entries table!
|
||||||
|
//
|
||||||
|
// May return *ConfigEntryGraphValidationError if there is a concern to surface
|
||||||
|
// to the caller that they can correct.
|
||||||
|
func (s *Store) validateProposedConfigEntryInGraph(
|
||||||
|
tx *memdb.Txn,
|
||||||
|
idx uint64,
|
||||||
|
kind, name string,
|
||||||
|
prev, next structs.ConfigEntry,
|
||||||
|
) error {
|
||||||
|
switch kind {
|
||||||
|
case structs.ProxyDefaults:
|
||||||
|
return nil // no validation
|
||||||
|
case structs.ServiceDefaults:
|
||||||
|
fallthrough
|
||||||
|
case structs.ServiceRouter:
|
||||||
|
fallthrough
|
||||||
|
case structs.ServiceSplitter:
|
||||||
|
fallthrough
|
||||||
|
case structs.ServiceResolver:
|
||||||
|
return s.validateProposedConfigEntryInServiceGraph(tx, idx, kind, name, prev, next)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unhandled kind %q during validation of %q", kind, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) validateProposedConfigEntryInServiceGraph(
|
||||||
|
tx *memdb.Txn,
|
||||||
|
idx uint64,
|
||||||
|
kind, name string,
|
||||||
|
prev, next structs.ConfigEntry,
|
||||||
|
) error {
|
||||||
|
// Collect all of the chains that could be affected by this change
|
||||||
|
// including our own.
|
||||||
|
checkChains := map[string]struct{}{
|
||||||
|
name: struct{}{},
|
||||||
|
}
|
||||||
|
|
||||||
|
iter, err := tx.Get(configTableName, "link", name)
|
||||||
|
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||||
|
entry := raw.(structs.ConfigEntry)
|
||||||
|
checkChains[entry.GetName()] = struct{}{}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
overrides := map[structs.ConfigEntryKindName]structs.ConfigEntry{
|
||||||
|
{Kind: kind, Name: name}: next,
|
||||||
|
}
|
||||||
|
|
||||||
|
for chainName, _ := range checkChains {
|
||||||
|
_, speculativeEntries, err := s.readDiscoveryChainConfigEntriesTxn(tx, nil, chainName, overrides)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// fmt.Printf("SPEC: %s/%s chain=%q, prev=%v, next=%v, ent=%+v\n",
|
||||||
|
// kind, name,
|
||||||
|
// chainName,
|
||||||
|
// prev != nil,
|
||||||
|
// next != nil, speculativeEntries)
|
||||||
|
|
||||||
|
// TODO(rb): is this ok that we execute the compiler in the state store?
|
||||||
|
|
||||||
|
// Note we use an arbitrary namespace and datacenter as those would not
|
||||||
|
// currently affect the graph compilation in ways that matter here.
|
||||||
|
req := discoverychain.CompileRequest{
|
||||||
|
ServiceName: chainName,
|
||||||
|
CurrentNamespace: "default",
|
||||||
|
CurrentDatacenter: "dc1",
|
||||||
|
Entries: speculativeEntries,
|
||||||
|
}
|
||||||
|
if _, err := discoverychain.Compile(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadDiscoveryChainConfigEntries will query for the full discovery chain for
|
||||||
|
// the provided service name. All relevant config entries will be recursively
|
||||||
|
// fetched and included in the result.
|
||||||
|
//
|
||||||
|
// Once returned, the caller still needs to assemble these into a useful graph
|
||||||
|
// structure.
|
||||||
|
func (s *Store) ReadDiscoveryChainConfigEntries(
|
||||||
|
ws memdb.WatchSet,
|
||||||
|
serviceName string,
|
||||||
|
) (uint64, *structs.DiscoveryChainConfigEntries, error) {
|
||||||
|
return s.readDiscoveryChainConfigEntries(ws, serviceName, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readDiscoveryChainConfigEntries will query for the full discovery chain for
|
||||||
|
// the provided service name. All relevant config entries will be recursively
|
||||||
|
// fetched and included in the result.
|
||||||
|
//
|
||||||
|
// If 'overrides' is provided then it will use entries in that map instead of
|
||||||
|
// the database to simulate the entries that go into a modified discovery chain
|
||||||
|
// without actually modifying it yet. Nil values are tombstones to simulate
|
||||||
|
// deleting an entry.
|
||||||
|
//
|
||||||
|
// Overrides is not mutated.
|
||||||
|
func (s *Store) readDiscoveryChainConfigEntries(
|
||||||
|
ws memdb.WatchSet,
|
||||||
|
serviceName string,
|
||||||
|
overrides map[structs.ConfigEntryKindName]structs.ConfigEntry,
|
||||||
|
) (uint64, *structs.DiscoveryChainConfigEntries, error) {
|
||||||
|
tx := s.db.Txn(false)
|
||||||
|
defer tx.Abort()
|
||||||
|
return s.readDiscoveryChainConfigEntriesTxn(tx, ws, serviceName, overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) readDiscoveryChainConfigEntriesTxn(
|
||||||
|
tx *memdb.Txn,
|
||||||
|
ws memdb.WatchSet,
|
||||||
|
serviceName string,
|
||||||
|
overrides map[structs.ConfigEntryKindName]structs.ConfigEntry,
|
||||||
|
) (uint64, *structs.DiscoveryChainConfigEntries, error) {
|
||||||
|
res := &structs.DiscoveryChainConfigEntries{
|
||||||
|
Routers: make(map[string]*structs.ServiceRouterConfigEntry),
|
||||||
|
Splitters: make(map[string]*structs.ServiceSplitterConfigEntry),
|
||||||
|
Resolvers: make(map[string]*structs.ServiceResolverConfigEntry),
|
||||||
|
Services: make(map[string]*structs.ServiceConfigEntry),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that below we always look up splitters and resolvers in pairs, even
|
||||||
|
// in some circumstances where both are not strictly necessary.
|
||||||
|
//
|
||||||
|
// For now we'll just eat the cost of fetching pairs of splitter/resolver
|
||||||
|
// config entries even though we may not always need both. In the common
|
||||||
|
// case we will need the pair so there's not a big drive to optimize this
|
||||||
|
// here at this time.
|
||||||
|
|
||||||
|
// Both Splitters and Resolvers maps will contain placeholder nils until
|
||||||
|
// the end of this function to indicate "no such entry".
|
||||||
|
|
||||||
|
var (
|
||||||
|
idx uint64
|
||||||
|
todoSplitters = make(map[string]struct{})
|
||||||
|
todoResolvers = make(map[string]struct{})
|
||||||
|
todoDefaults = make(map[string]struct{})
|
||||||
|
)
|
||||||
|
|
||||||
|
// At every step we'll need service defaults.
|
||||||
|
todoDefaults[serviceName] = struct{}{}
|
||||||
|
|
||||||
|
// first fetch the router, of which we only collect 1 per chain eval
|
||||||
|
_, router, err := s.getRouterConfigEntryTxn(tx, ws, serviceName, overrides)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
} else if router != nil {
|
||||||
|
res.Routers[serviceName] = router
|
||||||
|
}
|
||||||
|
|
||||||
|
if router != nil {
|
||||||
|
for _, svc := range router.ListRelatedServices() {
|
||||||
|
todoSplitters[svc] = struct{}{}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Next hop in the chain is the splitter.
|
||||||
|
todoSplitters[serviceName] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
name, ok := anyKey(todoSplitters)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
delete(todoSplitters, name)
|
||||||
|
|
||||||
|
if _, ok := res.Splitters[name]; ok {
|
||||||
|
continue // already fetched
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yes, even for splitters.
|
||||||
|
todoDefaults[name] = struct{}{}
|
||||||
|
|
||||||
|
_, splitter, err := s.getSplitterConfigEntryTxn(tx, ws, name, overrides)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if splitter == nil {
|
||||||
|
res.Splitters[name] = nil
|
||||||
|
|
||||||
|
// Next hop in the chain is the resolver.
|
||||||
|
todoResolvers[name] = struct{}{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Splitters[name] = splitter
|
||||||
|
|
||||||
|
todoResolvers[name] = struct{}{}
|
||||||
|
for _, svc := range splitter.ListRelatedServices() {
|
||||||
|
// If there is no splitter, this will end up adding a resolver
|
||||||
|
// after another iteration.
|
||||||
|
todoSplitters[svc] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
name, ok := anyKey(todoResolvers)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
delete(todoResolvers, name)
|
||||||
|
|
||||||
|
if _, ok := res.Resolvers[name]; ok {
|
||||||
|
continue // already fetched
|
||||||
|
}
|
||||||
|
|
||||||
|
// And resolvers, too.
|
||||||
|
todoDefaults[name] = struct{}{}
|
||||||
|
|
||||||
|
_, resolver, err := s.getResolverConfigEntryTxn(tx, ws, name, overrides)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resolver == nil {
|
||||||
|
res.Resolvers[name] = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Resolvers[name] = resolver
|
||||||
|
|
||||||
|
for _, svc := range resolver.ListRelatedServices() {
|
||||||
|
todoResolvers[svc] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
name, ok := anyKey(todoDefaults)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
delete(todoDefaults, name)
|
||||||
|
|
||||||
|
if _, ok := res.Services[name]; ok {
|
||||||
|
continue // already fetched
|
||||||
|
}
|
||||||
|
|
||||||
|
_, entry, err := s.getServiceConfigEntryTxn(tx, ws, name, overrides)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry == nil {
|
||||||
|
res.Services[name] = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Services[name] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip nils now that they are no longer necessary.
|
||||||
|
for name, entry := range res.Routers {
|
||||||
|
if entry == nil {
|
||||||
|
delete(res.Routers, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, entry := range res.Splitters {
|
||||||
|
if entry == nil {
|
||||||
|
delete(res.Splitters, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, entry := range res.Resolvers {
|
||||||
|
if entry == nil {
|
||||||
|
delete(res.Resolvers, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, entry := range res.Services {
|
||||||
|
if entry == nil {
|
||||||
|
delete(res.Services, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return idx, res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// anyKey returns any key from the provided map if any exist. Useful for using
|
||||||
|
// a map as a simple work queue of sorts.
|
||||||
|
func anyKey(m map[string]struct{}) (string, bool) {
|
||||||
|
if len(m) == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
for k, _ := range m {
|
||||||
|
return k, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// getServiceConfigEntryTxn is a convenience method for fetching a
|
||||||
|
// service-defaults kind of config entry.
|
||||||
|
//
|
||||||
|
// If an override is returned the index returned will be 0.
|
||||||
|
func (s *Store) getServiceConfigEntryTxn(
|
||||||
|
tx *memdb.Txn,
|
||||||
|
ws memdb.WatchSet,
|
||||||
|
serviceName string,
|
||||||
|
overrides map[structs.ConfigEntryKindName]structs.ConfigEntry,
|
||||||
|
) (uint64, *structs.ServiceConfigEntry, error) {
|
||||||
|
idx, entry, err := s.configEntryWithOverridesTxn(tx, ws, structs.ServiceDefaults, serviceName, overrides)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
} else if entry == nil {
|
||||||
|
return idx, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
service, ok := entry.(*structs.ServiceConfigEntry)
|
||||||
|
if !ok {
|
||||||
|
return 0, nil, fmt.Errorf("invalid service config type %T", entry)
|
||||||
|
}
|
||||||
|
return idx, service, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRouterConfigEntryTxn is a convenience method for fetching a
|
||||||
|
// service-router kind of config entry.
|
||||||
|
//
|
||||||
|
// If an override is returned the index returned will be 0.
|
||||||
|
func (s *Store) getRouterConfigEntryTxn(
|
||||||
|
tx *memdb.Txn,
|
||||||
|
ws memdb.WatchSet,
|
||||||
|
serviceName string,
|
||||||
|
overrides map[structs.ConfigEntryKindName]structs.ConfigEntry,
|
||||||
|
) (uint64, *structs.ServiceRouterConfigEntry, error) {
|
||||||
|
idx, entry, err := s.configEntryWithOverridesTxn(tx, ws, structs.ServiceRouter, serviceName, overrides)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
} else if entry == nil {
|
||||||
|
return idx, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
router, ok := entry.(*structs.ServiceRouterConfigEntry)
|
||||||
|
if !ok {
|
||||||
|
return 0, nil, fmt.Errorf("invalid service config type %T", entry)
|
||||||
|
}
|
||||||
|
return idx, router, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSplitterConfigEntryTxn is a convenience method for fetching a
|
||||||
|
// service-splitter kind of config entry.
|
||||||
|
//
|
||||||
|
// If an override is returned the index returned will be 0.
|
||||||
|
func (s *Store) getSplitterConfigEntryTxn(
|
||||||
|
tx *memdb.Txn,
|
||||||
|
ws memdb.WatchSet,
|
||||||
|
serviceName string,
|
||||||
|
overrides map[structs.ConfigEntryKindName]structs.ConfigEntry,
|
||||||
|
) (uint64, *structs.ServiceSplitterConfigEntry, error) {
|
||||||
|
idx, entry, err := s.configEntryWithOverridesTxn(tx, ws, structs.ServiceSplitter, serviceName, overrides)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
} else if entry == nil {
|
||||||
|
return idx, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
splitter, ok := entry.(*structs.ServiceSplitterConfigEntry)
|
||||||
|
if !ok {
|
||||||
|
return 0, nil, fmt.Errorf("invalid service config type %T", entry)
|
||||||
|
}
|
||||||
|
return idx, splitter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getResolverConfigEntryTxn is a convenience method for fetching a
|
||||||
|
// service-resolver kind of config entry.
|
||||||
|
//
|
||||||
|
// If an override is returned the index returned will be 0.
|
||||||
|
func (s *Store) getResolverConfigEntryTxn(
|
||||||
|
tx *memdb.Txn,
|
||||||
|
ws memdb.WatchSet,
|
||||||
|
serviceName string,
|
||||||
|
overrides map[structs.ConfigEntryKindName]structs.ConfigEntry,
|
||||||
|
) (uint64, *structs.ServiceResolverConfigEntry, error) {
|
||||||
|
idx, entry, err := s.configEntryWithOverridesTxn(tx, ws, structs.ServiceResolver, serviceName, overrides)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
} else if entry == nil {
|
||||||
|
return idx, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver, ok := entry.(*structs.ServiceResolverConfigEntry)
|
||||||
|
if !ok {
|
||||||
|
return 0, nil, fmt.Errorf("invalid service config type %T", entry)
|
||||||
|
}
|
||||||
|
return idx, resolver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) configEntryWithOverridesTxn(
|
||||||
|
tx *memdb.Txn,
|
||||||
|
ws memdb.WatchSet,
|
||||||
|
kind string,
|
||||||
|
name string,
|
||||||
|
overrides map[structs.ConfigEntryKindName]structs.ConfigEntry,
|
||||||
|
) (uint64, structs.ConfigEntry, error) {
|
||||||
|
if len(overrides) > 0 {
|
||||||
|
entry, ok := overrides[structs.ConfigEntryKindName{
|
||||||
|
Kind: kind, Name: name,
|
||||||
|
}]
|
||||||
|
if ok {
|
||||||
|
return 0, entry, nil // a nil entry implies it should act like it is erased
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.configEntryTxn(tx, ws, kind, name)
|
||||||
|
}
|
||||||
|
|
|
@ -1,287 +0,0 @@
|
||||||
package state
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
|
||||||
memdb "github.com/hashicorp/go-memdb"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReadDiscoveryChainConfigEntries will query for the full discovery chain for
|
|
||||||
// the provided service name. All relevant config entries will be recursively
|
|
||||||
// fetched and included in the result.
|
|
||||||
//
|
|
||||||
// Once returned, the caller still needs to assemble these into a useful graph
|
|
||||||
// structure.
|
|
||||||
func (s *Store) ReadDiscoveryChainConfigEntries(
|
|
||||||
ws memdb.WatchSet,
|
|
||||||
serviceName string,
|
|
||||||
) (uint64, *structs.DiscoveryChainConfigEntries, error) {
|
|
||||||
tx := s.db.Txn(false)
|
|
||||||
defer tx.Abort()
|
|
||||||
return s.readDiscoveryChainConfigEntriesTxn(tx, ws, serviceName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func allowDiscoveryChainL7Features(entry *structs.ServiceConfigEntry) bool {
|
|
||||||
if entry == nil {
|
|
||||||
return false // default is tcp
|
|
||||||
}
|
|
||||||
|
|
||||||
return structs.EnableAdvancedRoutingForProtocol(entry.Protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) readDiscoveryChainConfigEntriesTxn(
|
|
||||||
tx *memdb.Txn,
|
|
||||||
ws memdb.WatchSet,
|
|
||||||
serviceName string,
|
|
||||||
) (uint64, *structs.DiscoveryChainConfigEntries, error) {
|
|
||||||
// TODO(rb): improve this so you can simulate changes to vet writes.
|
|
||||||
|
|
||||||
res := &structs.DiscoveryChainConfigEntries{
|
|
||||||
Routers: make(map[string]*structs.ServiceRouterConfigEntry),
|
|
||||||
Splitters: make(map[string]*structs.ServiceSplitterConfigEntry),
|
|
||||||
Resolvers: make(map[string]*structs.ServiceResolverConfigEntry),
|
|
||||||
Services: make(map[string]*structs.ServiceConfigEntry),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note that below we always look up splitters and resolvers in pairs, even
|
|
||||||
// in some circumstances where both are not strictly necessary.
|
|
||||||
//
|
|
||||||
// For now we'll just eat the cost of fetching pairs of splitter/resolver
|
|
||||||
// config entries even though we may not always need both. In the common
|
|
||||||
// case we will need the pair so there's not a big drive to optimize this
|
|
||||||
// here at this time.
|
|
||||||
|
|
||||||
// Both Splitters and Resolvers maps will contain placeholder nils until
|
|
||||||
// the end of this function to indicate "no such entry".
|
|
||||||
|
|
||||||
var (
|
|
||||||
idx uint64
|
|
||||||
activateL7 = make(map[string]struct{})
|
|
||||||
todoSplitters = make(map[string]struct{})
|
|
||||||
todoResolvers = make(map[string]struct{})
|
|
||||||
)
|
|
||||||
|
|
||||||
checkL7 := func(name string) (bool, error) {
|
|
||||||
if _, loaded := res.Services[name]; loaded {
|
|
||||||
_, ok := activateL7[name]
|
|
||||||
return ok, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// first see if this is even a chain-aware protocol
|
|
||||||
thisIdx, entry, err := s.getServiceConfigEntryTxn(tx, ws, name)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if idx == 0 {
|
|
||||||
idx = thisIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Services[name] = entry // we'll strip the nil later
|
|
||||||
if allowDiscoveryChainL7Features(entry) {
|
|
||||||
activateL7[name] = struct{}{}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// first see if this is even a chain-aware protocol
|
|
||||||
if useL7, err := checkL7(serviceName); err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
|
|
||||||
} else if useL7 {
|
|
||||||
// first fetch the router, of which we only collect 1 per chain eval
|
|
||||||
_, router, err := s.getRouterConfigEntryTxn(tx, ws, serviceName)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
} else if router != nil {
|
|
||||||
res.Routers[serviceName] = router
|
|
||||||
}
|
|
||||||
|
|
||||||
if router != nil {
|
|
||||||
for _, svc := range router.ListRelatedServices() {
|
|
||||||
todoSplitters[svc] = struct{}{}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Next hop in the chain is the splitter.
|
|
||||||
todoSplitters[serviceName] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Next hop in the chain is the resolver.
|
|
||||||
res.Splitters[serviceName] = nil
|
|
||||||
todoResolvers[serviceName] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
name, ok := anyKey(todoSplitters)
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
delete(todoSplitters, name)
|
|
||||||
|
|
||||||
if _, ok := res.Splitters[name]; ok {
|
|
||||||
continue // already fetched
|
|
||||||
}
|
|
||||||
|
|
||||||
var splitter *structs.ServiceSplitterConfigEntry
|
|
||||||
if useL7, err := checkL7(name); err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
|
|
||||||
} else if useL7 {
|
|
||||||
_, splitter, err = s.getSplitterConfigEntryTxn(tx, ws, name)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
splitter = nil // sorry
|
|
||||||
}
|
|
||||||
|
|
||||||
if splitter == nil {
|
|
||||||
res.Splitters[name] = nil
|
|
||||||
|
|
||||||
// Next hop in the chain is the resolver.
|
|
||||||
todoResolvers[name] = struct{}{}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(splitter.Splits) == 0 {
|
|
||||||
return 0, nil, fmt.Errorf("found splitter config for %q that has no splits", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Splitters[name] = splitter
|
|
||||||
|
|
||||||
todoResolvers[name] = struct{}{}
|
|
||||||
for _, svc := range splitter.ListRelatedServices() {
|
|
||||||
// If there is no splitter, this will end up adding a resolver
|
|
||||||
// after another iteration.
|
|
||||||
todoSplitters[svc] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
name, ok := anyKey(todoResolvers)
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
delete(todoResolvers, name)
|
|
||||||
|
|
||||||
if _, ok := res.Resolvers[name]; ok {
|
|
||||||
continue // already fetched
|
|
||||||
}
|
|
||||||
|
|
||||||
_, resolver, err := s.getResolverConfigEntryTxn(tx, ws, name)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resolver == nil {
|
|
||||||
res.Resolvers[name] = nil
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(resolver.Failover) > 0 {
|
|
||||||
for subset, failoverClause := range resolver.Failover {
|
|
||||||
if failoverClause.Service == "" &&
|
|
||||||
failoverClause.ServiceSubset == "" &&
|
|
||||||
failoverClause.Namespace == "" &&
|
|
||||||
len(failoverClause.Datacenters) == 0 {
|
|
||||||
return 0, nil, fmt.Errorf("failover section for subset %q is errantly empty", subset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Resolvers[name] = resolver
|
|
||||||
|
|
||||||
for _, svc := range resolver.ListRelatedServices() {
|
|
||||||
todoResolvers[svc] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Fixup()
|
|
||||||
|
|
||||||
return idx, res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// anyKey returns any key from the provided map if any exist. Useful for using
|
|
||||||
// a map as a simple work queue of sorts.
|
|
||||||
func anyKey(m map[string]struct{}) (string, bool) {
|
|
||||||
if len(m) == 0 {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
for k, _ := range m {
|
|
||||||
return k, true
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
// getServiceConfigEntryTxn is a convenience method for fetching a
|
|
||||||
// service-defaults kind of config entry.
|
|
||||||
func (s *Store) getServiceConfigEntryTxn(tx *memdb.Txn, ws memdb.WatchSet, serviceName string) (uint64, *structs.ServiceConfigEntry, error) {
|
|
||||||
idx, entry, err := s.configEntryTxn(tx, ws, structs.ServiceDefaults, serviceName)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
} else if entry == nil {
|
|
||||||
return idx, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
service, ok := entry.(*structs.ServiceConfigEntry)
|
|
||||||
if !ok {
|
|
||||||
return 0, nil, fmt.Errorf("invalid service config type %T", entry)
|
|
||||||
}
|
|
||||||
return idx, service, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getRouterConfigEntryTxn is a convenience method for fetching a
|
|
||||||
// service-router kind of config entry.
|
|
||||||
func (s *Store) getRouterConfigEntryTxn(tx *memdb.Txn, ws memdb.WatchSet, serviceName string) (uint64, *structs.ServiceRouterConfigEntry, error) {
|
|
||||||
idx, entry, err := s.configEntryTxn(tx, ws, structs.ServiceRouter, serviceName)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
} else if entry == nil {
|
|
||||||
return idx, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
router, ok := entry.(*structs.ServiceRouterConfigEntry)
|
|
||||||
if !ok {
|
|
||||||
return 0, nil, fmt.Errorf("invalid service config type %T", entry)
|
|
||||||
}
|
|
||||||
return idx, router, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getSplitterConfigEntryTxn is a convenience method for fetching a
|
|
||||||
// service-splitter kind of config entry.
|
|
||||||
func (s *Store) getSplitterConfigEntryTxn(tx *memdb.Txn, ws memdb.WatchSet, serviceName string) (uint64, *structs.ServiceSplitterConfigEntry, error) {
|
|
||||||
idx, entry, err := s.configEntryTxn(tx, ws, structs.ServiceSplitter, serviceName)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
} else if entry == nil {
|
|
||||||
return idx, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
splitter, ok := entry.(*structs.ServiceSplitterConfigEntry)
|
|
||||||
if !ok {
|
|
||||||
return 0, nil, fmt.Errorf("invalid service config type %T", entry)
|
|
||||||
}
|
|
||||||
return idx, splitter, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getResolverConfigEntryTxn is a convenience method for fetching a
|
|
||||||
// service-resolver kind of config entry.
|
|
||||||
func (s *Store) getResolverConfigEntryTxn(tx *memdb.Txn, ws memdb.WatchSet, serviceName string) (uint64, *structs.ServiceResolverConfigEntry, error) {
|
|
||||||
idx, entry, err := s.configEntryTxn(tx, ws, structs.ServiceResolver, serviceName)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
} else if entry == nil {
|
|
||||||
return idx, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
resolver, ok := entry.(*structs.ServiceResolverConfigEntry)
|
|
||||||
if !ok {
|
|
||||||
return 0, nil, fmt.Errorf("invalid service config type %T", entry)
|
|
||||||
}
|
|
||||||
return idx, resolver, nil
|
|
||||||
}
|
|
|
@ -2,6 +2,7 @@ package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
memdb "github.com/hashicorp/go-memdb"
|
memdb "github.com/hashicorp/go-memdb"
|
||||||
|
@ -174,3 +175,825 @@ func TestStore_ConfigEntries(t *testing.T) {
|
||||||
}))
|
}))
|
||||||
require.True(watchFired(ws))
|
require.True(watchFired(ws))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStore_ConfigEntry_GraphValidation(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
entries []structs.ConfigEntry
|
||||||
|
op func(t *testing.T, s *Store) error
|
||||||
|
expectErr string
|
||||||
|
expectGraphErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "splitter fails without default protocol",
|
||||||
|
entries: []structs.ConfigEntry{},
|
||||||
|
op: func(t *testing.T, s *Store) error {
|
||||||
|
entry := &structs.ServiceSplitterConfigEntry{
|
||||||
|
Kind: structs.ServiceSplitter,
|
||||||
|
Name: "main",
|
||||||
|
Splits: []structs.ServiceSplit{
|
||||||
|
{Weight: 90, Namespace: "v1"},
|
||||||
|
{Weight: 10, Namespace: "v2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return s.EnsureConfigEntry(0, entry)
|
||||||
|
},
|
||||||
|
expectErr: "does not permit advanced routing or splitting behavior",
|
||||||
|
expectGraphErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "splitter fails with tcp protocol",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "tcp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
op: func(t *testing.T, s *Store) error {
|
||||||
|
entry := &structs.ServiceSplitterConfigEntry{
|
||||||
|
Kind: structs.ServiceSplitter,
|
||||||
|
Name: "main",
|
||||||
|
Splits: []structs.ServiceSplit{
|
||||||
|
{Weight: 90, Namespace: "v1"},
|
||||||
|
{Weight: 10, Namespace: "v2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return s.EnsureConfigEntry(0, entry)
|
||||||
|
},
|
||||||
|
expectErr: "does not permit advanced routing or splitting behavior",
|
||||||
|
expectGraphErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "router fails with tcp protocol",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "tcp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
op: func(t *testing.T, s *Store) error {
|
||||||
|
entry := &structs.ServiceRouterConfigEntry{
|
||||||
|
Kind: structs.ServiceRouter,
|
||||||
|
Name: "main",
|
||||||
|
Routes: []structs.ServiceRoute{
|
||||||
|
{
|
||||||
|
Match: &structs.ServiceRouteMatch{
|
||||||
|
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "/other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &structs.ServiceRouteDestination{
|
||||||
|
Namespace: "other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return s.EnsureConfigEntry(0, entry)
|
||||||
|
},
|
||||||
|
expectErr: "does not permit advanced routing or splitting behavior",
|
||||||
|
expectGraphErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "router fails without default protocol",
|
||||||
|
entries: []structs.ConfigEntry{},
|
||||||
|
op: func(t *testing.T, s *Store) error {
|
||||||
|
entry := &structs.ServiceRouterConfigEntry{
|
||||||
|
Kind: structs.ServiceRouter,
|
||||||
|
Name: "main",
|
||||||
|
Routes: []structs.ServiceRoute{
|
||||||
|
{
|
||||||
|
Match: &structs.ServiceRouteMatch{
|
||||||
|
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "/other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &structs.ServiceRouteDestination{
|
||||||
|
Namespace: "other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return s.EnsureConfigEntry(0, entry)
|
||||||
|
},
|
||||||
|
expectErr: "does not permit advanced routing or splitting behavior",
|
||||||
|
expectGraphErr: true,
|
||||||
|
},
|
||||||
|
/////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
name: "cannot remove default protocol after splitter created",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "http",
|
||||||
|
},
|
||||||
|
&structs.ServiceSplitterConfigEntry{
|
||||||
|
Kind: structs.ServiceSplitter,
|
||||||
|
Name: "main",
|
||||||
|
Splits: []structs.ServiceSplit{
|
||||||
|
{Weight: 90, Namespace: "v1"},
|
||||||
|
{Weight: 10, Namespace: "v2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
op: func(t *testing.T, s *Store) error {
|
||||||
|
return s.DeleteConfigEntry(0, structs.ServiceDefaults, "main")
|
||||||
|
},
|
||||||
|
expectErr: "does not permit advanced routing or splitting behavior",
|
||||||
|
expectGraphErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cannot change to tcp protocol after splitter created",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "http",
|
||||||
|
},
|
||||||
|
&structs.ServiceSplitterConfigEntry{
|
||||||
|
Kind: structs.ServiceSplitter,
|
||||||
|
Name: "main",
|
||||||
|
Splits: []structs.ServiceSplit{
|
||||||
|
{Weight: 90, Namespace: "v1"},
|
||||||
|
{Weight: 10, Namespace: "v2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
op: func(t *testing.T, s *Store) error {
|
||||||
|
entry := &structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "tcp",
|
||||||
|
}
|
||||||
|
return s.EnsureConfigEntry(0, entry)
|
||||||
|
},
|
||||||
|
expectErr: "does not permit advanced routing or splitting behavior",
|
||||||
|
expectGraphErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cannot remove default protocol after router created",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "http",
|
||||||
|
},
|
||||||
|
&structs.ServiceRouterConfigEntry{
|
||||||
|
Kind: structs.ServiceRouter,
|
||||||
|
Name: "main",
|
||||||
|
Routes: []structs.ServiceRoute{
|
||||||
|
{
|
||||||
|
Match: &structs.ServiceRouteMatch{
|
||||||
|
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "/other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &structs.ServiceRouteDestination{
|
||||||
|
Namespace: "other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
op: func(t *testing.T, s *Store) error {
|
||||||
|
return s.DeleteConfigEntry(0, structs.ServiceDefaults, "main")
|
||||||
|
},
|
||||||
|
expectErr: "does not permit advanced routing or splitting behavior",
|
||||||
|
expectGraphErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cannot change to tcp protocol after router created",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "http",
|
||||||
|
},
|
||||||
|
&structs.ServiceRouterConfigEntry{
|
||||||
|
Kind: structs.ServiceRouter,
|
||||||
|
Name: "main",
|
||||||
|
Routes: []structs.ServiceRoute{
|
||||||
|
{
|
||||||
|
Match: &structs.ServiceRouteMatch{
|
||||||
|
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "/other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &structs.ServiceRouteDestination{
|
||||||
|
Namespace: "other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
op: func(t *testing.T, s *Store) error {
|
||||||
|
entry := &structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "tcp",
|
||||||
|
}
|
||||||
|
return s.EnsureConfigEntry(0, entry)
|
||||||
|
},
|
||||||
|
expectErr: "does not permit advanced routing or splitting behavior",
|
||||||
|
expectGraphErr: true,
|
||||||
|
},
|
||||||
|
/////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
name: "cannot split to a service using tcp",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "http",
|
||||||
|
},
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "other",
|
||||||
|
Protocol: "tcp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
op: func(t *testing.T, s *Store) error {
|
||||||
|
entry := &structs.ServiceSplitterConfigEntry{
|
||||||
|
Kind: structs.ServiceSplitter,
|
||||||
|
Name: "main",
|
||||||
|
Splits: []structs.ServiceSplit{
|
||||||
|
{Weight: 90},
|
||||||
|
{Weight: 10, Service: "other"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return s.EnsureConfigEntry(0, entry)
|
||||||
|
},
|
||||||
|
expectErr: "uses inconsistent protocols",
|
||||||
|
expectGraphErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cannot route to a service using tcp",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "http",
|
||||||
|
},
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "other",
|
||||||
|
Protocol: "tcp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
op: func(t *testing.T, s *Store) error {
|
||||||
|
entry := &structs.ServiceRouterConfigEntry{
|
||||||
|
Kind: structs.ServiceRouter,
|
||||||
|
Name: "main",
|
||||||
|
Routes: []structs.ServiceRoute{
|
||||||
|
{
|
||||||
|
Match: &structs.ServiceRouteMatch{
|
||||||
|
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "/other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &structs.ServiceRouteDestination{
|
||||||
|
Service: "other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return s.EnsureConfigEntry(0, entry)
|
||||||
|
},
|
||||||
|
expectErr: "uses inconsistent protocols",
|
||||||
|
expectGraphErr: true,
|
||||||
|
},
|
||||||
|
/////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
name: "cannot failover to a service using a different protocol",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "grpc",
|
||||||
|
},
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "other",
|
||||||
|
Protocol: "tcp",
|
||||||
|
},
|
||||||
|
&structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "main",
|
||||||
|
ConnectTimeout: 33 * time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
op: func(t *testing.T, s *Store) error {
|
||||||
|
entry := &structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "main",
|
||||||
|
Failover: map[string]structs.ServiceResolverFailover{
|
||||||
|
"*": structs.ServiceResolverFailover{
|
||||||
|
Service: "other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return s.EnsureConfigEntry(0, entry)
|
||||||
|
},
|
||||||
|
expectErr: "uses inconsistent protocols",
|
||||||
|
expectGraphErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cannot redirect to a service using a different protocol",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "grpc",
|
||||||
|
},
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "other",
|
||||||
|
Protocol: "tcp",
|
||||||
|
},
|
||||||
|
&structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "main",
|
||||||
|
ConnectTimeout: 33 * time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
op: func(t *testing.T, s *Store) error {
|
||||||
|
entry := &structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "main",
|
||||||
|
Redirect: &structs.ServiceResolverRedirect{
|
||||||
|
Service: "other",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return s.EnsureConfigEntry(0, entry)
|
||||||
|
},
|
||||||
|
expectErr: "uses inconsistent protocols",
|
||||||
|
expectGraphErr: true,
|
||||||
|
},
|
||||||
|
/////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
name: "redirect to a subset that does exist is fine",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "other",
|
||||||
|
ConnectTimeout: 33 * time.Second,
|
||||||
|
Subsets: map[string]structs.ServiceResolverSubset{
|
||||||
|
"v1": structs.ServiceResolverSubset{
|
||||||
|
Filter: "Service.Meta.version == v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
op: func(t *testing.T, s *Store) error {
|
||||||
|
entry := &structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "main",
|
||||||
|
Redirect: &structs.ServiceResolverRedirect{
|
||||||
|
Service: "other",
|
||||||
|
ServiceSubset: "v1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return s.EnsureConfigEntry(0, entry)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cannot redirect to a subset that does not exist",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "other",
|
||||||
|
ConnectTimeout: 33 * time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
op: func(t *testing.T, s *Store) error {
|
||||||
|
entry := &structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "main",
|
||||||
|
Redirect: &structs.ServiceResolverRedirect{
|
||||||
|
Service: "other",
|
||||||
|
ServiceSubset: "v1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return s.EnsureConfigEntry(0, entry)
|
||||||
|
},
|
||||||
|
expectErr: `does not have a subset named "v1"`,
|
||||||
|
expectGraphErr: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
tc := tc
|
||||||
|
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
s := testStateStore(t)
|
||||||
|
for _, entry := range tc.entries {
|
||||||
|
require.NoError(t, s.EnsureConfigEntry(0, entry))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tc.op(t, s)
|
||||||
|
if tc.expectErr != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.expectErr)
|
||||||
|
_, ok := err.(*structs.ConfigEntryGraphError)
|
||||||
|
if tc.expectGraphErr {
|
||||||
|
require.True(t, ok, "%T is not a *ConfigEntryGraphError", err)
|
||||||
|
} else {
|
||||||
|
require.False(t, ok, "did not expect a *ConfigEntryGraphError here: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
entries []structs.ConfigEntry
|
||||||
|
expectBefore []structs.ConfigEntryKindName
|
||||||
|
overrides map[structs.ConfigEntryKindName]structs.ConfigEntry
|
||||||
|
expectAfter []structs.ConfigEntryKindName
|
||||||
|
expectAfterErr string
|
||||||
|
checkAfter func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "mask service-defaults",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "tcp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectBefore: []structs.ConfigEntryKindName{
|
||||||
|
{Kind: structs.ServiceDefaults, Name: "main"},
|
||||||
|
},
|
||||||
|
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
|
||||||
|
{Kind: structs.ServiceDefaults, Name: "main"}: nil,
|
||||||
|
},
|
||||||
|
expectAfter: []structs.ConfigEntryKindName{
|
||||||
|
// nothing
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "edit service-defaults",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "tcp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectBefore: []structs.ConfigEntryKindName{
|
||||||
|
{Kind: structs.ServiceDefaults, Name: "main"},
|
||||||
|
},
|
||||||
|
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
|
||||||
|
{Kind: structs.ServiceDefaults, Name: "main"}: &structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "grpc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectAfter: []structs.ConfigEntryKindName{
|
||||||
|
{Kind: structs.ServiceDefaults, Name: "main"},
|
||||||
|
},
|
||||||
|
checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) {
|
||||||
|
defaults := entrySet.GetService("main")
|
||||||
|
require.NotNil(t, defaults)
|
||||||
|
require.Equal(t, "grpc", defaults.Protocol)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "mask service-router",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "http",
|
||||||
|
},
|
||||||
|
&structs.ServiceRouterConfigEntry{
|
||||||
|
Kind: structs.ServiceRouter,
|
||||||
|
Name: "main",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectBefore: []structs.ConfigEntryKindName{
|
||||||
|
{Kind: structs.ServiceDefaults, Name: "main"},
|
||||||
|
{Kind: structs.ServiceRouter, Name: "main"},
|
||||||
|
},
|
||||||
|
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
|
||||||
|
{Kind: structs.ServiceRouter, Name: "main"}: nil,
|
||||||
|
},
|
||||||
|
expectAfter: []structs.ConfigEntryKindName{
|
||||||
|
{Kind: structs.ServiceDefaults, Name: "main"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "edit service-router",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "http",
|
||||||
|
},
|
||||||
|
&structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "main",
|
||||||
|
Subsets: map[string]structs.ServiceResolverSubset{
|
||||||
|
"v1": {Filter: "Service.Meta.version == v1"},
|
||||||
|
"v2": {Filter: "Service.Meta.version == v2"},
|
||||||
|
"v3": {Filter: "Service.Meta.version == v3"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&structs.ServiceRouterConfigEntry{
|
||||||
|
Kind: structs.ServiceRouter,
|
||||||
|
Name: "main",
|
||||||
|
Routes: []structs.ServiceRoute{
|
||||||
|
{
|
||||||
|
Match: &structs.ServiceRouteMatch{
|
||||||
|
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "/admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &structs.ServiceRouteDestination{
|
||||||
|
ServiceSubset: "v2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectBefore: []structs.ConfigEntryKindName{
|
||||||
|
{Kind: structs.ServiceDefaults, Name: "main"},
|
||||||
|
{Kind: structs.ServiceResolver, Name: "main"},
|
||||||
|
{Kind: structs.ServiceRouter, Name: "main"},
|
||||||
|
},
|
||||||
|
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
|
||||||
|
{Kind: structs.ServiceRouter, Name: "main"}: &structs.ServiceRouterConfigEntry{
|
||||||
|
Kind: structs.ServiceRouter,
|
||||||
|
Name: "main",
|
||||||
|
Routes: []structs.ServiceRoute{
|
||||||
|
{
|
||||||
|
Match: &structs.ServiceRouteMatch{
|
||||||
|
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "/admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &structs.ServiceRouteDestination{
|
||||||
|
ServiceSubset: "v3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectAfter: []structs.ConfigEntryKindName{
|
||||||
|
{Kind: structs.ServiceDefaults, Name: "main"},
|
||||||
|
{Kind: structs.ServiceResolver, Name: "main"},
|
||||||
|
{Kind: structs.ServiceRouter, Name: "main"},
|
||||||
|
},
|
||||||
|
checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) {
|
||||||
|
router := entrySet.GetRouter("main")
|
||||||
|
require.NotNil(t, router)
|
||||||
|
require.Len(t, router.Routes, 1)
|
||||||
|
|
||||||
|
expect := structs.ServiceRoute{
|
||||||
|
Match: &structs.ServiceRouteMatch{
|
||||||
|
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||||
|
PathExact: "/admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &structs.ServiceRouteDestination{
|
||||||
|
ServiceSubset: "v3",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Equal(t, expect, router.Routes[0])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "mask service-splitter",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "http",
|
||||||
|
},
|
||||||
|
&structs.ServiceSplitterConfigEntry{
|
||||||
|
Kind: structs.ServiceSplitter,
|
||||||
|
Name: "main",
|
||||||
|
Splits: []structs.ServiceSplit{
|
||||||
|
{Weight: 100},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectBefore: []structs.ConfigEntryKindName{
|
||||||
|
{Kind: structs.ServiceDefaults, Name: "main"},
|
||||||
|
{Kind: structs.ServiceSplitter, Name: "main"},
|
||||||
|
},
|
||||||
|
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
|
||||||
|
{Kind: structs.ServiceSplitter, Name: "main"}: nil,
|
||||||
|
},
|
||||||
|
expectAfter: []structs.ConfigEntryKindName{
|
||||||
|
{Kind: structs.ServiceDefaults, Name: "main"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "edit service-splitter",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "http",
|
||||||
|
},
|
||||||
|
&structs.ServiceSplitterConfigEntry{
|
||||||
|
Kind: structs.ServiceSplitter,
|
||||||
|
Name: "main",
|
||||||
|
Splits: []structs.ServiceSplit{
|
||||||
|
{Weight: 100},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectBefore: []structs.ConfigEntryKindName{
|
||||||
|
{Kind: structs.ServiceDefaults, Name: "main"},
|
||||||
|
{Kind: structs.ServiceSplitter, Name: "main"},
|
||||||
|
},
|
||||||
|
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
|
||||||
|
{Kind: structs.ServiceSplitter, Name: "main"}: &structs.ServiceSplitterConfigEntry{
|
||||||
|
Kind: structs.ServiceSplitter,
|
||||||
|
Name: "main",
|
||||||
|
Splits: []structs.ServiceSplit{
|
||||||
|
{Weight: 85, ServiceSubset: "v1"},
|
||||||
|
{Weight: 15, ServiceSubset: "v2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectAfter: []structs.ConfigEntryKindName{
|
||||||
|
{Kind: structs.ServiceDefaults, Name: "main"},
|
||||||
|
{Kind: structs.ServiceSplitter, Name: "main"},
|
||||||
|
},
|
||||||
|
checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) {
|
||||||
|
splitter := entrySet.GetSplitter("main")
|
||||||
|
require.NotNil(t, splitter)
|
||||||
|
require.Len(t, splitter.Splits, 2)
|
||||||
|
|
||||||
|
expect := []structs.ServiceSplit{
|
||||||
|
{Weight: 85, ServiceSubset: "v1"},
|
||||||
|
{Weight: 15, ServiceSubset: "v2"},
|
||||||
|
}
|
||||||
|
require.Equal(t, expect, splitter.Splits)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "mask service-resolver",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "main",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectBefore: []structs.ConfigEntryKindName{
|
||||||
|
{Kind: structs.ServiceResolver, Name: "main"},
|
||||||
|
},
|
||||||
|
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
|
||||||
|
{Kind: structs.ServiceResolver, Name: "main"}: nil,
|
||||||
|
},
|
||||||
|
expectAfter: []structs.ConfigEntryKindName{
|
||||||
|
// nothing
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "edit service-resolver",
|
||||||
|
entries: []structs.ConfigEntry{
|
||||||
|
&structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "main",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectBefore: []structs.ConfigEntryKindName{
|
||||||
|
{Kind: structs.ServiceResolver, Name: "main"},
|
||||||
|
},
|
||||||
|
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
|
||||||
|
{Kind: structs.ServiceResolver, Name: "main"}: &structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "main",
|
||||||
|
ConnectTimeout: 33 * time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectAfter: []structs.ConfigEntryKindName{
|
||||||
|
{Kind: structs.ServiceResolver, Name: "main"},
|
||||||
|
},
|
||||||
|
checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) {
|
||||||
|
resolver := entrySet.GetResolver("main")
|
||||||
|
require.NotNil(t, resolver)
|
||||||
|
require.Equal(t, 33*time.Second, resolver.ConnectTimeout)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
tc := tc
|
||||||
|
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
s := testStateStore(t)
|
||||||
|
for _, entry := range tc.entries {
|
||||||
|
require.NoError(t, s.EnsureConfigEntry(0, entry))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("without override", func(t *testing.T) {
|
||||||
|
_, entrySet, err := s.readDiscoveryChainConfigEntries(nil, "main", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
got := entrySetToKindNames(entrySet)
|
||||||
|
require.ElementsMatch(t, tc.expectBefore, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with override", func(t *testing.T) {
|
||||||
|
_, entrySet, err := s.readDiscoveryChainConfigEntries(nil, "main", tc.overrides)
|
||||||
|
|
||||||
|
if tc.expectAfterErr != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.expectAfterErr)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
got := entrySetToKindNames(entrySet)
|
||||||
|
require.ElementsMatch(t, tc.expectAfter, got)
|
||||||
|
|
||||||
|
if tc.checkAfter != nil {
|
||||||
|
tc.checkAfter(t, entrySet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func entrySetToKindNames(entrySet *structs.DiscoveryChainConfigEntries) []structs.ConfigEntryKindName {
|
||||||
|
var out []structs.ConfigEntryKindName
|
||||||
|
for _, entry := range entrySet.Routers {
|
||||||
|
out = append(out, structs.ConfigEntryKindName{
|
||||||
|
Kind: entry.Kind,
|
||||||
|
Name: entry.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, entry := range entrySet.Splitters {
|
||||||
|
out = append(out, structs.ConfigEntryKindName{
|
||||||
|
Kind: entry.Kind,
|
||||||
|
Name: entry.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, entry := range entrySet.Resolvers {
|
||||||
|
out = append(out, structs.ConfigEntryKindName{
|
||||||
|
Kind: entry.Kind,
|
||||||
|
Name: entry.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, entry := range entrySet.Services {
|
||||||
|
out = append(out, structs.ConfigEntryKindName{
|
||||||
|
Kind: entry.Kind,
|
||||||
|
Name: entry.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStore_ReadDiscoveryChainConfigEntries_SubsetSplit(t *testing.T) {
|
||||||
|
s := testStateStore(t)
|
||||||
|
|
||||||
|
entries := []structs.ConfigEntry{
|
||||||
|
&structs.ServiceConfigEntry{
|
||||||
|
Kind: structs.ServiceDefaults,
|
||||||
|
Name: "main",
|
||||||
|
Protocol: "http",
|
||||||
|
},
|
||||||
|
&structs.ServiceResolverConfigEntry{
|
||||||
|
Kind: structs.ServiceResolver,
|
||||||
|
Name: "main",
|
||||||
|
Subsets: map[string]structs.ServiceResolverSubset{
|
||||||
|
"v1": structs.ServiceResolverSubset{
|
||||||
|
Filter: "Service.Meta.version == v1",
|
||||||
|
},
|
||||||
|
"v2": structs.ServiceResolverSubset{
|
||||||
|
Filter: "Service.Meta.version == v2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&structs.ServiceSplitterConfigEntry{
|
||||||
|
Kind: structs.ServiceSplitter,
|
||||||
|
Name: "main",
|
||||||
|
Splits: []structs.ServiceSplit{
|
||||||
|
{Weight: 90, ServiceSubset: "v1"},
|
||||||
|
{Weight: 10, ServiceSubset: "v2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
require.NoError(t, s.EnsureConfigEntry(0, entry))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, entrySet, err := s.ReadDiscoveryChainConfigEntries(nil, "main")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, entrySet.Routers, 0)
|
||||||
|
require.Len(t, entrySet.Splitters, 1)
|
||||||
|
require.Len(t, entrySet.Resolvers, 1)
|
||||||
|
require.Len(t, entrySet.Services, 1)
|
||||||
|
}
|
||||||
|
|
|
@ -542,3 +542,12 @@ func (c *ConfigEntryResponse) UnmarshalBinary(data []byte) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfigEntryKindName is a value type useful for maps. You can use:
|
||||||
|
// map[ConfigEntryKindName]Payload
|
||||||
|
// instead of:
|
||||||
|
// map[string]map[string]Payload
|
||||||
|
type ConfigEntryKindName struct {
|
||||||
|
Kind string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
|
@ -9,15 +9,6 @@ import (
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
)
|
)
|
||||||
|
|
||||||
func EnableAdvancedRoutingForProtocol(protocol string) bool {
|
|
||||||
switch protocol {
|
|
||||||
case "http", "http2", "grpc":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceRouterConfigEntry defines L7 (e.g. http) routing rules for a named
|
// ServiceRouterConfigEntry defines L7 (e.g. http) routing rules for a named
|
||||||
// service exposed in Connect.
|
// service exposed in Connect.
|
||||||
//
|
//
|
||||||
|
@ -60,6 +51,8 @@ func (e *ServiceRouterConfigEntry) Normalize() error {
|
||||||
return fmt.Errorf("config entry is nil")
|
return fmt.Errorf("config entry is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(rb): trim spaces
|
||||||
|
|
||||||
e.Kind = ServiceRouter
|
e.Kind = ServiceRouter
|
||||||
|
|
||||||
// TODO(rb): anything to normalize?
|
// TODO(rb): anything to normalize?
|
||||||
|
@ -74,6 +67,8 @@ func (e *ServiceRouterConfigEntry) Validate() error {
|
||||||
|
|
||||||
// TODO(rb): enforce corresponding service has protocol=http
|
// TODO(rb): enforce corresponding service has protocol=http
|
||||||
|
|
||||||
|
// TODO(rb): actually you can only define the HTTP section if protocol=http{,2}
|
||||||
|
|
||||||
// TODO(rb): validate the entire compiled chain? how?
|
// TODO(rb): validate the entire compiled chain? how?
|
||||||
|
|
||||||
// TODO(rb): validate more
|
// TODO(rb): validate more
|
||||||
|
@ -82,6 +77,69 @@ func (e *ServiceRouterConfigEntry) Validate() error {
|
||||||
// catch-all is configured for you, but at that point maybe you should just
|
// catch-all is configured for you, but at that point maybe you should just
|
||||||
// delete it so it will default?
|
// delete it so it will default?
|
||||||
|
|
||||||
|
for i, route := range e.Routes {
|
||||||
|
if route.Match == nil || route.Match.HTTP == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pathParts := 0
|
||||||
|
if route.Match.HTTP.PathExact != "" {
|
||||||
|
pathParts++
|
||||||
|
}
|
||||||
|
if route.Match.HTTP.PathPrefix != "" {
|
||||||
|
pathParts++
|
||||||
|
}
|
||||||
|
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++
|
||||||
|
}
|
||||||
|
// "absent" is the bare invert=true
|
||||||
|
if (hdrParts == 0 && !hdr.Invert) || (hdrParts > 1) {
|
||||||
|
return fmt.Errorf("Route[%d] Header[%d] should only contain one of Present, Exact, Prefix, Suffix, or Regex (or just Invert)", 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ineligibleForPrefixRewrite := false
|
||||||
|
if route.Match.HTTP.PathRegex != "" {
|
||||||
|
ineligibleForPrefixRewrite = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if route.Destination != nil {
|
||||||
|
if route.Destination.PrefixRewrite != "" && ineligibleForPrefixRewrite {
|
||||||
|
return fmt.Errorf("Route[%d] cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,6 +194,10 @@ type ServiceRouteMatch struct {
|
||||||
// (gRPC, redis, etc) they can go here.
|
// (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.
|
// ServiceRouteHTTPMatch is a set of http-specific match criteria.
|
||||||
type ServiceRouteHTTPMatch struct {
|
type ServiceRouteHTTPMatch struct {
|
||||||
PathExact string `json:",omitempty"`
|
PathExact string `json:",omitempty"`
|
||||||
|
@ -145,7 +207,17 @@ type ServiceRouteHTTPMatch struct {
|
||||||
Header []ServiceRouteHTTPMatchHeader `json:",omitempty"`
|
Header []ServiceRouteHTTPMatchHeader `json:",omitempty"`
|
||||||
QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty"`
|
QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty"`
|
||||||
|
|
||||||
Methods []string `json:",omitempty"`
|
// TODO(rb): reenable Methods
|
||||||
|
// 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 {
|
type ServiceRouteHTTPMatchHeader struct {
|
||||||
|
@ -255,6 +327,7 @@ func (e *ServiceSplitterConfigEntry) Normalize() error {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return fmt.Errorf("config entry is nil")
|
return fmt.Errorf("config entry is nil")
|
||||||
}
|
}
|
||||||
|
// TODO(rb): trim spaces
|
||||||
|
|
||||||
e.Kind = ServiceSplitter
|
e.Kind = ServiceSplitter
|
||||||
|
|
||||||
|
@ -451,6 +524,17 @@ type ServiceResolverConfigEntry struct {
|
||||||
RaftIndex
|
RaftIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
func (e *ServiceResolverConfigEntry) IsDefault() bool {
|
||||||
return e.DefaultSubset == "" &&
|
return e.DefaultSubset == "" &&
|
||||||
len(e.Subsets) == 0 &&
|
len(e.Subsets) == 0 &&
|
||||||
|
@ -475,6 +559,7 @@ func (e *ServiceResolverConfigEntry) Normalize() error {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return fmt.Errorf("config entry is nil")
|
return fmt.Errorf("config entry is nil")
|
||||||
}
|
}
|
||||||
|
// TODO(rb): trim spaces
|
||||||
|
|
||||||
e.Kind = ServiceResolver
|
e.Kind = ServiceResolver
|
||||||
|
|
||||||
|
@ -511,6 +596,12 @@ func (e *ServiceResolverConfigEntry) Validate() error {
|
||||||
if e.Redirect != nil {
|
if e.Redirect != nil {
|
||||||
r := e.Redirect
|
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 == "" {
|
if r.Service == "" && r.ServiceSubset == "" && r.Namespace == "" && r.Datacenter == "" {
|
||||||
return fmt.Errorf("Redirect is empty")
|
return fmt.Errorf("Redirect is empty")
|
||||||
}
|
}
|
||||||
|
@ -538,8 +629,8 @@ func (e *ServiceResolverConfigEntry) Validate() error {
|
||||||
return fmt.Errorf("Bad Failover[%q]: not a valid subset", subset)
|
return fmt.Errorf("Bad Failover[%q]: not a valid subset", subset)
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Service == "" && f.ServiceSubset == "" && len(f.Datacenters) == 0 {
|
if f.Service == "" && f.ServiceSubset == "" && f.Namespace == "" && len(f.Datacenters) == 0 {
|
||||||
return fmt.Errorf("Bad Failover[%q] one of Service, ServiceSubset, or Datacenters is required", subset)
|
return fmt.Errorf("Bad Failover[%q] one of Service, ServiceSubset, Namespace, or Datacenters is required", subset)
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.ServiceSubset != "" {
|
if f.ServiceSubset != "" {
|
||||||
|
@ -750,64 +841,6 @@ type DiscoveryChainConfigEntries struct {
|
||||||
Services map[string]*ServiceConfigEntry
|
Services map[string]*ServiceConfigEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixup ensures that the collection of entries make sense together. Nil maps
|
|
||||||
// are initialized, nil values are deleted, and advanced features are disabled
|
|
||||||
// if protocol dictates.
|
|
||||||
func (e *DiscoveryChainConfigEntries) Fixup() {
|
|
||||||
if e.Services == nil {
|
|
||||||
e.Services = make(map[string]*ServiceConfigEntry)
|
|
||||||
}
|
|
||||||
if e.Routers == nil {
|
|
||||||
e.Routers = make(map[string]*ServiceRouterConfigEntry)
|
|
||||||
}
|
|
||||||
if e.Splitters == nil {
|
|
||||||
e.Splitters = make(map[string]*ServiceSplitterConfigEntry)
|
|
||||||
}
|
|
||||||
if e.Resolvers == nil {
|
|
||||||
e.Resolvers = make(map[string]*ServiceResolverConfigEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, entry := range e.Routers {
|
|
||||||
if entry == nil {
|
|
||||||
delete(e.Routers, name)
|
|
||||||
} else {
|
|
||||||
if !e.allowAdvancedRouting(name) {
|
|
||||||
delete(e.Routers, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for name, entry := range e.Splitters {
|
|
||||||
if entry == nil {
|
|
||||||
delete(e.Splitters, name)
|
|
||||||
} else {
|
|
||||||
if !e.allowAdvancedRouting(name) {
|
|
||||||
delete(e.Splitters, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for name, entry := range e.Resolvers {
|
|
||||||
if entry == nil {
|
|
||||||
delete(e.Resolvers, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for name, entry := range e.Services {
|
|
||||||
if entry == nil {
|
|
||||||
delete(e.Services, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *DiscoveryChainConfigEntries) allowAdvancedRouting(name string) bool {
|
|
||||||
if e.Services == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
entry, ok := e.Services[name]
|
|
||||||
if !ok || entry == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return EnableAdvancedRoutingForProtocol(entry.Protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *DiscoveryChainConfigEntries) GetRouter(name string) *ServiceRouterConfigEntry {
|
func (e *DiscoveryChainConfigEntries) GetRouter(name string) *ServiceRouterConfigEntry {
|
||||||
if e.Routers != nil {
|
if e.Routers != nil {
|
||||||
return e.Routers[name]
|
return e.Routers[name]
|
||||||
|
@ -883,3 +916,16 @@ func (e *DiscoveryChainConfigEntries) IsEmpty() bool {
|
||||||
func (e *DiscoveryChainConfigEntries) IsChainEmpty() bool {
|
func (e *DiscoveryChainConfigEntries) IsChainEmpty() bool {
|
||||||
return len(e.Routers) == 0 && len(e.Splitters) == 0 && len(e.Resolvers) == 0
|
return len(e.Routers) == 0 && len(e.Splitters) == 0 && len(e.Resolvers) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ func TestServiceResolverConfigEntry(t *testing.T) {
|
||||||
Name: "test",
|
Name: "test",
|
||||||
DefaultSubset: "gone",
|
DefaultSubset: "gone",
|
||||||
Subsets: map[string]ServiceResolverSubset{
|
Subsets: map[string]ServiceResolverSubset{
|
||||||
"v1": {Filter: "ServiceMeta.version == v1"},
|
"v1": {Filter: "Service.Meta.version == v1"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
validateErr: `DefaultSubset "gone" is not a valid subset`,
|
validateErr: `DefaultSubset "gone" is not a valid subset`,
|
||||||
|
@ -65,7 +65,7 @@ func TestServiceResolverConfigEntry(t *testing.T) {
|
||||||
Name: "test",
|
Name: "test",
|
||||||
DefaultSubset: "v1",
|
DefaultSubset: "v1",
|
||||||
Subsets: map[string]ServiceResolverSubset{
|
Subsets: map[string]ServiceResolverSubset{
|
||||||
"v1": {Filter: "ServiceMeta.version == v1"},
|
"v1": {Filter: "Service.Meta.version == v1"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -122,7 +122,7 @@ func TestServiceResolverConfigEntry(t *testing.T) {
|
||||||
ServiceSubset: "v1",
|
ServiceSubset: "v1",
|
||||||
},
|
},
|
||||||
Subsets: map[string]ServiceResolverSubset{
|
Subsets: map[string]ServiceResolverSubset{
|
||||||
"v1": {Filter: "ServiceMeta.version == v1"},
|
"v1": {Filter: "Service.Meta.version == v1"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -157,7 +157,7 @@ func TestServiceResolverConfigEntry(t *testing.T) {
|
||||||
Kind: ServiceResolver,
|
Kind: ServiceResolver,
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Subsets: map[string]ServiceResolverSubset{
|
Subsets: map[string]ServiceResolverSubset{
|
||||||
"v1": {Filter: "ServiceMeta.version == v1"},
|
"v1": {Filter: "Service.Meta.version == v1"},
|
||||||
},
|
},
|
||||||
Failover: map[string]ServiceResolverFailover{
|
Failover: map[string]ServiceResolverFailover{
|
||||||
"v1": ServiceResolverFailover{
|
"v1": ServiceResolverFailover{
|
||||||
|
@ -172,13 +172,13 @@ func TestServiceResolverConfigEntry(t *testing.T) {
|
||||||
Kind: ServiceResolver,
|
Kind: ServiceResolver,
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Subsets: map[string]ServiceResolverSubset{
|
Subsets: map[string]ServiceResolverSubset{
|
||||||
"v1": {Filter: "ServiceMeta.version == v1"},
|
"v1": {Filter: "Service.Meta.version == v1"},
|
||||||
},
|
},
|
||||||
Failover: map[string]ServiceResolverFailover{
|
Failover: map[string]ServiceResolverFailover{
|
||||||
"v1": ServiceResolverFailover{},
|
"v1": ServiceResolverFailover{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
validateErr: `Bad Failover["v1"] one of Service, ServiceSubset, or Datacenters is required`,
|
validateErr: `Bad Failover["v1"] one of Service, ServiceSubset, Namespace, or Datacenters is required`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "failover to self using invalid subset",
|
name: "failover to self using invalid subset",
|
||||||
|
@ -186,7 +186,7 @@ func TestServiceResolverConfigEntry(t *testing.T) {
|
||||||
Kind: ServiceResolver,
|
Kind: ServiceResolver,
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Subsets: map[string]ServiceResolverSubset{
|
Subsets: map[string]ServiceResolverSubset{
|
||||||
"v1": {Filter: "ServiceMeta.version == v1"},
|
"v1": {Filter: "Service.Meta.version == v1"},
|
||||||
},
|
},
|
||||||
Failover: map[string]ServiceResolverFailover{
|
Failover: map[string]ServiceResolverFailover{
|
||||||
"v1": ServiceResolverFailover{
|
"v1": ServiceResolverFailover{
|
||||||
|
@ -203,8 +203,8 @@ func TestServiceResolverConfigEntry(t *testing.T) {
|
||||||
Kind: ServiceResolver,
|
Kind: ServiceResolver,
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Subsets: map[string]ServiceResolverSubset{
|
Subsets: map[string]ServiceResolverSubset{
|
||||||
"v1": {Filter: "ServiceMeta.version == v1"},
|
"v1": {Filter: "Service.Meta.version == v1"},
|
||||||
"v2": {Filter: "ServiceMeta.version == v2"},
|
"v2": {Filter: "Service.Meta.version == v2"},
|
||||||
},
|
},
|
||||||
Failover: map[string]ServiceResolverFailover{
|
Failover: map[string]ServiceResolverFailover{
|
||||||
"v1": ServiceResolverFailover{
|
"v1": ServiceResolverFailover{
|
||||||
|
|
|
@ -34,7 +34,8 @@ type ServiceRouteHTTPMatch struct {
|
||||||
Header []ServiceRouteHTTPMatchHeader `json:",omitempty"`
|
Header []ServiceRouteHTTPMatchHeader `json:",omitempty"`
|
||||||
QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty"`
|
QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty"`
|
||||||
|
|
||||||
Methods []string `json:",omitempty"`
|
// TODO(rb): reenable Methods
|
||||||
|
// Methods []string `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServiceRouteHTTPMatchHeader struct {
|
type ServiceRouteHTTPMatchHeader struct {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -14,97 +15,48 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) {
|
||||||
|
|
||||||
config_entries := c.ConfigEntries()
|
config_entries := c.ConfigEntries()
|
||||||
|
|
||||||
t.Run("Service Router", func(t *testing.T) {
|
verifyResolver := func(t *testing.T, initial ConfigEntry) {
|
||||||
// use one mega object to avoid multiple trips
|
t.Helper()
|
||||||
makeEntry := func() *ServiceRouterConfigEntry {
|
require.IsType(t, &ServiceResolverConfigEntry{}, initial)
|
||||||
return &ServiceRouterConfigEntry{
|
testEntry := initial.(*ServiceResolverConfigEntry)
|
||||||
Kind: ServiceRouter,
|
|
||||||
Name: "test",
|
|
||||||
Routes: []ServiceRoute{
|
|
||||||
{
|
|
||||||
Match: &ServiceRouteMatch{
|
|
||||||
HTTP: &ServiceRouteHTTPMatch{
|
|
||||||
PathPrefix: "/prefix",
|
|
||||||
Header: []ServiceRouteHTTPMatchHeader{
|
|
||||||
{Name: "x-debug", Exact: "1"},
|
|
||||||
},
|
|
||||||
QueryParam: []ServiceRouteHTTPMatchQueryParam{
|
|
||||||
{Name: "debug", Value: "1"},
|
|
||||||
},
|
|
||||||
Methods: []string{"GET", "POST"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Destination: &ServiceRouteDestination{
|
|
||||||
Service: "other",
|
|
||||||
ServiceSubset: "v2",
|
|
||||||
Namespace: "sec",
|
|
||||||
PrefixRewrite: "/",
|
|
||||||
RequestTimeout: 5 * time.Second,
|
|
||||||
NumRetries: 5,
|
|
||||||
RetryOnConnectFailure: true,
|
|
||||||
RetryOnStatusCodes: []uint32{500, 503, 401},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set it
|
// set it
|
||||||
_, wm, err := config_entries.Set(makeEntry(), nil)
|
_, wm, err := config_entries.Set(testEntry, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, wm)
|
require.NotNil(t, wm)
|
||||||
require.NotEqual(t, 0, wm.RequestTime)
|
require.NotEqual(t, 0, wm.RequestTime)
|
||||||
|
|
||||||
// get it
|
// get it
|
||||||
entry, qm, err := config_entries.Get(ServiceRouter, "test", nil)
|
entry, qm, err := config_entries.Get(ServiceResolver, testEntry.Name, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, qm)
|
require.NotNil(t, qm)
|
||||||
require.NotEqual(t, 0, qm.RequestTime)
|
require.NotEqual(t, 0, qm.RequestTime)
|
||||||
|
|
||||||
// verify it
|
// verify it
|
||||||
readRouter, ok := entry.(*ServiceRouterConfigEntry)
|
readResolver, ok := entry.(*ServiceResolverConfigEntry)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
readRouter.ModifyIndex = 0 // reset for Equals()
|
readResolver.ModifyIndex = 0 // reset for Equals()
|
||||||
readRouter.CreateIndex = 0 // reset for Equals()
|
readResolver.CreateIndex = 0 // reset for Equals()
|
||||||
|
|
||||||
goldenEntry := makeEntry()
|
require.Equal(t, testEntry, readResolver)
|
||||||
require.Equal(t, goldenEntry, readRouter)
|
|
||||||
|
|
||||||
// TODO(rb): cas?
|
// TODO(rb): cas?
|
||||||
// TODO(rb): list?
|
// TODO(rb): list?
|
||||||
})
|
}
|
||||||
|
|
||||||
t.Run("Service Splitter", func(t *testing.T) {
|
verifySplitter := func(t *testing.T, initial ConfigEntry) {
|
||||||
// use one mega object to avoid multiple trips
|
t.Helper()
|
||||||
makeEntry := func() *ServiceSplitterConfigEntry {
|
require.IsType(t, &ServiceSplitterConfigEntry{}, initial)
|
||||||
return &ServiceSplitterConfigEntry{
|
testEntry := initial.(*ServiceSplitterConfigEntry)
|
||||||
Kind: ServiceSplitter,
|
|
||||||
Name: "test",
|
|
||||||
Splits: []ServiceSplit{
|
|
||||||
{
|
|
||||||
Weight: 90,
|
|
||||||
Service: "a",
|
|
||||||
ServiceSubset: "b",
|
|
||||||
Namespace: "c",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Weight: 10,
|
|
||||||
Service: "x",
|
|
||||||
ServiceSubset: "y",
|
|
||||||
Namespace: "z",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set it
|
// set it
|
||||||
_, wm, err := config_entries.Set(makeEntry(), nil)
|
_, wm, err := config_entries.Set(testEntry, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, wm)
|
require.NotNil(t, wm)
|
||||||
require.NotEqual(t, 0, wm.RequestTime)
|
require.NotEqual(t, 0, wm.RequestTime)
|
||||||
|
|
||||||
// get it
|
// get it
|
||||||
entry, qm, err := config_entries.Get(ServiceSplitter, "test", nil)
|
entry, qm, err := config_entries.Get(ServiceSplitter, testEntry.Name, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, qm)
|
require.NotNil(t, qm)
|
||||||
require.NotEqual(t, 0, qm.RequestTime)
|
require.NotEqual(t, 0, qm.RequestTime)
|
||||||
|
@ -115,37 +67,76 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) {
|
||||||
readSplitter.ModifyIndex = 0 // reset for Equals()
|
readSplitter.ModifyIndex = 0 // reset for Equals()
|
||||||
readSplitter.CreateIndex = 0 // reset for Equals()
|
readSplitter.CreateIndex = 0 // reset for Equals()
|
||||||
|
|
||||||
goldenEntry := makeEntry()
|
require.Equal(t, testEntry, readSplitter)
|
||||||
require.Equal(t, goldenEntry, readSplitter)
|
|
||||||
|
|
||||||
// TODO(rb): cas?
|
// TODO(rb): cas?
|
||||||
// TODO(rb): list?
|
// TODO(rb): list?
|
||||||
})
|
|
||||||
|
|
||||||
for name, tc := range map[string]func() *ServiceResolverConfigEntry{
|
|
||||||
"with-redirect": func() *ServiceResolverConfigEntry {
|
|
||||||
return &ServiceResolverConfigEntry{
|
|
||||||
Kind: ServiceResolver,
|
|
||||||
Name: "test",
|
|
||||||
Redirect: &ServiceResolverRedirect{
|
|
||||||
Service: "a",
|
|
||||||
ServiceSubset: "b",
|
|
||||||
Namespace: "c",
|
|
||||||
Datacenter: "d",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"no-redirect": func() *ServiceResolverConfigEntry {
|
verifyRouter := func(t *testing.T, initial ConfigEntry) {
|
||||||
return &ServiceResolverConfigEntry{
|
t.Helper()
|
||||||
|
require.IsType(t, &ServiceRouterConfigEntry{}, initial)
|
||||||
|
testEntry := initial.(*ServiceRouterConfigEntry)
|
||||||
|
|
||||||
|
// set it
|
||||||
|
_, wm, err := config_entries.Set(testEntry, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, wm)
|
||||||
|
require.NotEqual(t, 0, wm.RequestTime)
|
||||||
|
|
||||||
|
// get it
|
||||||
|
entry, qm, err := config_entries.Get(ServiceRouter, testEntry.Name, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, qm)
|
||||||
|
require.NotEqual(t, 0, qm.RequestTime)
|
||||||
|
|
||||||
|
// verify it
|
||||||
|
readRouter, ok := entry.(*ServiceRouterConfigEntry)
|
||||||
|
require.True(t, ok)
|
||||||
|
readRouter.ModifyIndex = 0 // reset for Equals()
|
||||||
|
readRouter.CreateIndex = 0 // reset for Equals()
|
||||||
|
|
||||||
|
require.Equal(t, testEntry, readRouter)
|
||||||
|
|
||||||
|
// TODO(rb): cas?
|
||||||
|
// TODO(rb): list?
|
||||||
|
}
|
||||||
|
|
||||||
|
// First set the necessary protocols to allow advanced routing features.
|
||||||
|
for _, service := range []string{
|
||||||
|
"test-failover",
|
||||||
|
"test-redirect",
|
||||||
|
"alternate",
|
||||||
|
"test-split",
|
||||||
|
"test-route",
|
||||||
|
} {
|
||||||
|
serviceDefaults := &ServiceConfigEntry{
|
||||||
|
Kind: ServiceDefaults,
|
||||||
|
Name: service,
|
||||||
|
Protocol: "http",
|
||||||
|
}
|
||||||
|
_, _, err := config_entries.Set(serviceDefaults, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Due to service graph validation, these have to happen in a specific order.
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
entry ConfigEntry
|
||||||
|
verify func(t *testing.T, initial ConfigEntry)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "failover",
|
||||||
|
entry: &ServiceResolverConfigEntry{
|
||||||
Kind: ServiceResolver,
|
Kind: ServiceResolver,
|
||||||
Name: "test",
|
Name: "test-failover",
|
||||||
DefaultSubset: "v1",
|
DefaultSubset: "v1",
|
||||||
Subsets: map[string]ServiceResolverSubset{
|
Subsets: map[string]ServiceResolverSubset{
|
||||||
"v1": ServiceResolverSubset{
|
"v1": ServiceResolverSubset{
|
||||||
Filter: "ServiceMeta.version == v1",
|
Filter: "Service.Meta.version == v1",
|
||||||
},
|
},
|
||||||
"v2": ServiceResolverSubset{
|
"v2": ServiceResolverSubset{
|
||||||
Filter: "ServiceMeta.version == v2",
|
Filter: "Service.Meta.version == v2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Failover: map[string]ServiceResolverFailover{
|
Failover: map[string]ServiceResolverFailover{
|
||||||
|
@ -157,36 +148,83 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ConnectTimeout: 5 * time.Second,
|
ConnectTimeout: 5 * time.Second,
|
||||||
}
|
},
|
||||||
|
verify: verifyResolver,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "redirect",
|
||||||
|
entry: &ServiceResolverConfigEntry{
|
||||||
|
Kind: ServiceResolver,
|
||||||
|
Name: "test-redirect",
|
||||||
|
Redirect: &ServiceResolverRedirect{
|
||||||
|
Service: "test-failover",
|
||||||
|
ServiceSubset: "v2",
|
||||||
|
Namespace: "c",
|
||||||
|
Datacenter: "d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verify: verifyResolver,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mega splitter", // use one mega object to avoid multiple trips
|
||||||
|
entry: &ServiceSplitterConfigEntry{
|
||||||
|
Kind: ServiceSplitter,
|
||||||
|
Name: "test-split",
|
||||||
|
Splits: []ServiceSplit{
|
||||||
|
{
|
||||||
|
Weight: 90,
|
||||||
|
Service: "test-failover",
|
||||||
|
ServiceSubset: "v1",
|
||||||
|
Namespace: "c",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Weight: 10,
|
||||||
|
Service: "test-redirect",
|
||||||
|
Namespace: "z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verify: verifySplitter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mega router", // use one mega object to avoid multiple trips
|
||||||
|
entry: &ServiceRouterConfigEntry{
|
||||||
|
Kind: ServiceRouter,
|
||||||
|
Name: "test-route",
|
||||||
|
Routes: []ServiceRoute{
|
||||||
|
{
|
||||||
|
Match: &ServiceRouteMatch{
|
||||||
|
HTTP: &ServiceRouteHTTPMatch{
|
||||||
|
PathPrefix: "/prefix",
|
||||||
|
Header: []ServiceRouteHTTPMatchHeader{
|
||||||
|
{Name: "x-debug", Exact: "1"},
|
||||||
|
},
|
||||||
|
QueryParam: []ServiceRouteHTTPMatchQueryParam{
|
||||||
|
{Name: "debug", Value: "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &ServiceRouteDestination{
|
||||||
|
Service: "test-failover",
|
||||||
|
ServiceSubset: "v2",
|
||||||
|
Namespace: "sec",
|
||||||
|
PrefixRewrite: "/",
|
||||||
|
RequestTimeout: 5 * time.Second,
|
||||||
|
NumRetries: 5,
|
||||||
|
RetryOnConnectFailure: true,
|
||||||
|
RetryOnStatusCodes: []uint32{500, 503, 401},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verify: verifyRouter,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
// use one mega object to avoid multiple trips
|
tc := tc
|
||||||
makeEntry := tc
|
name := fmt.Sprintf("%s:%s: %s", tc.entry.GetKind(), tc.entry.GetName(), tc.name)
|
||||||
t.Run("Service Resolver - "+name, func(t *testing.T) {
|
ok := t.Run(name, func(t *testing.T) {
|
||||||
|
tc.verify(t, tc.entry)
|
||||||
// set it
|
|
||||||
_, wm, err := config_entries.Set(makeEntry(), nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, wm)
|
|
||||||
require.NotEqual(t, 0, wm.RequestTime)
|
|
||||||
|
|
||||||
// get it
|
|
||||||
entry, qm, err := config_entries.Get(ServiceResolver, "test", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, qm)
|
|
||||||
require.NotEqual(t, 0, qm.RequestTime)
|
|
||||||
|
|
||||||
// verify it
|
|
||||||
readResolver, ok := entry.(*ServiceResolverConfigEntry)
|
|
||||||
require.True(t, ok)
|
|
||||||
readResolver.ModifyIndex = 0 // reset for Equals()
|
|
||||||
readResolver.CreateIndex = 0 // reset for Equals()
|
|
||||||
|
|
||||||
goldenEntry := makeEntry()
|
|
||||||
require.Equal(t, goldenEntry, readResolver)
|
|
||||||
|
|
||||||
// TODO(rb): cas?
|
|
||||||
// TODO(rb): list?
|
|
||||||
})
|
})
|
||||||
|
require.True(t, ok, "subtest %q failed so aborting remainder", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,7 +170,6 @@ func TestAPI_ConfigEntries(t *testing.T) {
|
||||||
require.Equal(t, service2.Name, readService.Name)
|
require.Equal(t, service2.Name, readService.Name)
|
||||||
require.Equal(t, service2.Protocol, readService.Protocol)
|
require.Equal(t, service2.Protocol, readService.Protocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete it
|
// delete it
|
||||||
|
|
Loading…
Reference in New Issue