package discoverychain import ( "testing" "time" "github.com/hashicorp/consul/agent/structs" "github.com/stretchr/testify/require" ) func TestCompile_NoEntries_NoInferDefaults(t *testing.T) { entries := newEntries() res, err := Compile(CompileRequest{ ServiceName: "main", CurrentNamespace: "default", CurrentDatacenter: "dc1", InferDefaults: false, Entries: entries, }) require.NoError(t, err) require.Nil(t, res) } type compileTestCase struct { entries *structs.DiscoveryChainConfigEntries // expect: the GroupResolverNodes map should have nil values expect *structs.CompiledDiscoveryChain // expectIsDefault tests behavior of CompiledDiscoveryChain.IsDefault() expectIsDefault bool expectErr string expectGraphErr bool } func TestCompile(t *testing.T) { t.Parallel() // TODO(rb): test circular dependency? cases := map[string]compileTestCase{ "router with defaults": testcase_JustRouterWithDefaults(), "router with defaults and resolver": testcase_RouterWithDefaults_NoSplit_WithResolver(), "router with defaults and noop split": testcase_RouterWithDefaults_WithNoopSplit_DefaultResolver(), "router with defaults and noop split and resolver": testcase_RouterWithDefaults_WithNoopSplit_WithResolver(), "route bypasses splitter": testcase_RouteBypassesSplit(), "noop split": testcase_NoopSplit_DefaultResolver(), "noop split with protocol from proxy defaults": testcase_NoopSplit_DefaultResolver_ProtocolFromProxyDefaults(), "noop split with resolver": testcase_NoopSplit_WithResolver(), "subset split": testcase_SubsetSplit(), "service split": testcase_ServiceSplit(), "split bypasses next splitter": testcase_SplitBypassesSplit(), "service redirect": testcase_ServiceRedirect(), "service and subset redirect": testcase_ServiceAndSubsetRedirect(), "datacenter redirect": testcase_DatacenterRedirect(), "service failover": testcase_ServiceFailover(), "service and subset failover": testcase_ServiceAndSubsetFailover(), "datacenter failover": testcase_DatacenterFailover(), "noop split to resolver with default subset": testcase_NoopSplit_WithDefaultSubset(), "resolver with default subset": testcase_Resolve_WithDefaultSubset(), "resolver with no entries and inferring defaults": testcase_DefaultResolver(), "default resolver with proxy defaults": testcase_DefaultResolver_WithProxyDefaults(), "service redirect to service with default resolver is not a default chain": testcase_RedirectToDefaultResolverIsNotDefaultChain(), // TODO(rb): handle this case better: "circular split": testcase_CircularSplit(), "all the bells and whistles": testcase_AllBellsAndWhistles(), "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 { tc := tc t.Run(name, func(t *testing.T) { t.Parallel() // sanity check entries are normalized and valid for _, entry := range tc.entries.Routers { require.NoError(t, entry.Normalize()) require.NoError(t, entry.Validate()) } for _, entry := range tc.entries.Splitters { require.NoError(t, entry.Normalize()) require.NoError(t, entry.Validate()) } for _, entry := range tc.entries.Resolvers { require.NoError(t, entry.Normalize()) require.NoError(t, entry.Validate()) } 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) // Avoid requiring unnecessary test boilerplate and inject these // ourselves. tc.expect.ServiceName = "main" tc.expect.Namespace = "default" tc.expect.Datacenter = "dc1" // These nodes are duplicated elsewhere in the results, so we only // care that the keys are present. Walk the results and nil out the // value payloads so that the require.Equal will still do the work // for us. if len(res.GroupResolverNodes) > 0 { for target, _ := range res.GroupResolverNodes { res.GroupResolverNodes[target] = nil } } require.Equal(t, tc.expect, res) require.Equal(t, tc.expectIsDefault, res.IsDefault()) } }) } } func testcase_JustRouterWithDefaults() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") entries.AddRouters( &structs.ServiceRouterConfigEntry{ Kind: "service-router", Name: "main", }, ) resolver := newDefaultServiceResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "http", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeRouter, Name: "main", Routes: []*structs.DiscoveryRoute{ { Definition: newDefaultServiceRoute("main"), DestinationNode: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, Default: true, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "", "default", "dc1"), }, }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_RouterWithDefaults_NoSplit_WithResolver() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") entries.AddRouters( &structs.ServiceRouterConfigEntry{ Kind: "service-router", Name: "main", }, ) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", ConnectTimeout: 33 * time.Second, }, ) resolver := entries.GetResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "http", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeRouter, Name: "main", Routes: []*structs.DiscoveryRoute{ { Definition: newDefaultServiceRoute("main"), DestinationNode: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, ConnectTimeout: 33 * time.Second, Target: newTarget("main", "", "default", "dc1"), }, }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_RouterWithDefaults_WithNoopSplit_DefaultResolver() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") entries.AddRouters( &structs.ServiceRouterConfigEntry{ Kind: "service-router", Name: "main", }, ) entries.AddSplitters( &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "main", Splits: []structs.ServiceSplit{ {Weight: 100}, }, }, ) resolver := newDefaultServiceResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "http", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeRouter, Name: "main", Routes: []*structs.DiscoveryRoute{ { Definition: newDefaultServiceRoute("main"), DestinationNode: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main", Splits: []*structs.DiscoverySplit{ { Weight: 100, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, Default: true, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "", "default", "dc1"), }, }, }, }, }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_NoopSplit_DefaultResolver_ProtocolFromProxyDefaults() compileTestCase { entries := newEntries() setGlobalProxyProtocol(entries, "http") entries.AddRouters( &structs.ServiceRouterConfigEntry{ Kind: "service-router", Name: "main", }, ) entries.AddSplitters( &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "main", Splits: []structs.ServiceSplit{ {Weight: 100}, }, }, ) resolver := newDefaultServiceResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "http", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeRouter, Name: "main", Routes: []*structs.DiscoveryRoute{ { Definition: newDefaultServiceRoute("main"), DestinationNode: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main", Splits: []*structs.DiscoverySplit{ { Weight: 100, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, Default: true, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "", "default", "dc1"), }, }, }, }, }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_RouterWithDefaults_WithNoopSplit_WithResolver() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") entries.AddRouters( &structs.ServiceRouterConfigEntry{ Kind: "service-router", Name: "main", }, ) entries.AddSplitters( &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "main", Splits: []structs.ServiceSplit{ {Weight: 100}, }, }, ) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", ConnectTimeout: 33 * time.Second, }, ) resolver := entries.GetResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "http", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeRouter, Name: "main", Routes: []*structs.DiscoveryRoute{ { Definition: newDefaultServiceRoute("main"), DestinationNode: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main", Splits: []*structs.DiscoverySplit{ { Weight: 100, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, ConnectTimeout: 33 * time.Second, Target: newTarget("main", "", "default", "dc1"), }, }, }, }, }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_RouteBypassesSplit() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") setServiceProtocol(entries, "other", "http") entries.AddRouters( &structs.ServiceRouterConfigEntry{ Kind: "service-router", Name: "main", Routes: []structs.ServiceRoute{ // route direct subset reference (bypass split) newSimpleRoute("other", func(r *structs.ServiceRoute) { r.Destination.ServiceSubset = "bypass" }), }, }, ) entries.AddSplitters( &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "other", Splits: []structs.ServiceSplit{ {Weight: 100, Service: "ignored"}, }, }, ) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "other", Subsets: map[string]structs.ServiceResolverSubset{ "bypass": { Filter: "Service.Meta.version == bypass", }, }, }, ) router := entries.GetRouter("main") resolverOther := entries.GetResolver("other") resolverMain := newDefaultServiceResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "http", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeRouter, Name: "main", Routes: []*structs.DiscoveryRoute{ { Definition: &router.Routes[0], DestinationNode: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "other", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolverOther, ConnectTimeout: 5 * time.Second, Target: newTarget("other", "bypass", "default", "dc1"), }, }, }, { Definition: newDefaultServiceRoute("main"), DestinationNode: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolverMain, Default: true, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "", "default", "dc1"), }, }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "other": resolverOther, "main": resolverMain, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "", "default", "dc1"), newTarget("other", "bypass", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "", "default", "dc1"): nil, newTarget("other", "bypass", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_NoopSplit_DefaultResolver() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") entries.AddSplitters( &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "main", Splits: []structs.ServiceSplit{ {Weight: 100}, }, }, ) resolver := newDefaultServiceResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "http", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main", Splits: []*structs.DiscoverySplit{ { Weight: 100, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, Default: true, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "", "default", "dc1"), }, }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_NoopSplit_WithResolver() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") entries.AddSplitters( &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "main", Splits: []structs.ServiceSplit{ {Weight: 100}, }, }, ) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", ConnectTimeout: 33 * time.Second, }, ) resolver := entries.GetResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "http", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main", Splits: []*structs.DiscoverySplit{ { Weight: 100, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, ConnectTimeout: 33 * time.Second, Target: newTarget("main", "", "default", "dc1"), }, }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_SubsetSplit() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") entries.AddSplitters( &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "main", Splits: []structs.ServiceSplit{ {Weight: 60, ServiceSubset: "v2"}, {Weight: 40, ServiceSubset: "v1"}, }, }, ) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Subsets: map[string]structs.ServiceResolverSubset{ "v1": { Filter: "Service.Meta.version == 1", }, "v2": { Filter: "Service.Meta.version == 2", }, }, }, ) resolver := entries.GetResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "http", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main", Splits: []*structs.DiscoverySplit{ { Weight: 60, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "v2", "default", "dc1"), }, }, }, { Weight: 40, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "v1", "default", "dc1"), }, }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "v1", "default", "dc1"), newTarget("main", "v2", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "v1", "default", "dc1"): nil, newTarget("main", "v2", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_ServiceSplit() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") setServiceProtocol(entries, "foo", "http") setServiceProtocol(entries, "bar", "http") entries.AddSplitters( &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "main", Splits: []structs.ServiceSplit{ {Weight: 60, Service: "foo"}, {Weight: 40, Service: "bar"}, }, }, ) resolverFoo := newDefaultServiceResolver("foo") resolverBar := newDefaultServiceResolver("bar") expect := &structs.CompiledDiscoveryChain{ Protocol: "http", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main", Splits: []*structs.DiscoverySplit{ { Weight: 60, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "foo", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolverFoo, Default: true, ConnectTimeout: 5 * time.Second, Target: newTarget("foo", "", "default", "dc1"), }, }, }, { Weight: 40, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "bar", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolverBar, Default: true, ConnectTimeout: 5 * time.Second, Target: newTarget("bar", "", "default", "dc1"), }, }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "foo": resolverFoo, "bar": resolverBar, }, Targets: []structs.DiscoveryTarget{ newTarget("bar", "", "default", "dc1"), newTarget("foo", "", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("bar", "", "default", "dc1"): nil, newTarget("foo", "", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_SplitBypassesSplit() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") setServiceProtocol(entries, "next", "http") entries.AddSplitters( &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "main", Splits: []structs.ServiceSplit{ { Weight: 100, Service: "next", ServiceSubset: "bypassed", }, }, }, &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "next", Splits: []structs.ServiceSplit{ { Weight: 100, ServiceSubset: "not-bypassed", }, }, }, ) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "next", Subsets: map[string]structs.ServiceResolverSubset{ "bypassed": { Filter: "Service.Meta.version == bypass", }, "not-bypassed": { Filter: "Service.Meta.version != bypass", }, }, }, ) resolverNext := entries.GetResolver("next") expect := &structs.CompiledDiscoveryChain{ Protocol: "http", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main", Splits: []*structs.DiscoverySplit{ { Weight: 100, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "next", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolverNext, ConnectTimeout: 5 * time.Second, Target: newTarget("next", "bypassed", "default", "dc1"), }, }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "next": resolverNext, }, Targets: []structs.DiscoveryTarget{ newTarget("next", "bypassed", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("next", "bypassed", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_ServiceRedirect() compileTestCase { entries := newEntries() entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", }, }, ) resolverOther := newDefaultServiceResolver("other") expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "other", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolverOther, Default: true, ConnectTimeout: 5 * time.Second, Target: newTarget("other", "", "default", "dc1"), }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "other": resolverOther, }, Targets: []structs.DiscoveryTarget{ newTarget("other", "", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("other", "", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_ServiceAndSubsetRedirect() compileTestCase { entries := newEntries() entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", ServiceSubset: "v2", }, }, &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "other", Subsets: map[string]structs.ServiceResolverSubset{ "v1": { Filter: "Service.Meta.version == 1", }, "v2": { Filter: "Service.Meta.version == 2", }, }, }, ) resolver := entries.GetResolver("other") expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "other", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, ConnectTimeout: 5 * time.Second, Target: newTarget("other", "v2", "default", "dc1"), }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "other": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("other", "v2", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("other", "v2", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_DatacenterRedirect() compileTestCase { entries := newEntries() entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Redirect: &structs.ServiceResolverRedirect{ Datacenter: "dc9", }, }, ) resolver := entries.GetResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "", "default", "dc9"), }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "", "default", "dc9"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "", "default", "dc9"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_ServiceFailover() compileTestCase { entries := newEntries() entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Failover: map[string]structs.ServiceResolverFailover{ "*": {Service: "backup"}, }, }, ) resolverMain := entries.GetResolver("main") resolverBackup := newDefaultServiceResolver("backup") wildFail := resolverMain.Failover["*"] expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolverMain, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "", "default", "dc1"), Failover: &structs.DiscoveryFailover{ Definition: &wildFail, Targets: []structs.DiscoveryTarget{ newTarget("backup", "", "default", "dc1"), }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolverMain, "backup": resolverBackup, }, Targets: []structs.DiscoveryTarget{ newTarget("backup", "", "default", "dc1"), newTarget("main", "", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_ServiceAndSubsetFailover() compileTestCase { entries := newEntries() entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Subsets: map[string]structs.ServiceResolverSubset{ "backup": { Filter: "Service.Meta.version == backup", }, }, Failover: map[string]structs.ServiceResolverFailover{ "*": {ServiceSubset: "backup"}, }, }, ) resolver := entries.GetResolver("main") wildFail := resolver.Failover["*"] expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "", "default", "dc1"), Failover: &structs.DiscoveryFailover{ Definition: &wildFail, Targets: []structs.DiscoveryTarget{ newTarget("main", "backup", "default", "dc1"), }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "", "default", "dc1"), newTarget("main", "backup", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_DatacenterFailover() compileTestCase { entries := newEntries() entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Failover: map[string]structs.ServiceResolverFailover{ "*": {Datacenters: []string{"dc2", "dc4"}}, }, }, ) resolver := entries.GetResolver("main") wildFail := resolver.Failover["*"] expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "", "default", "dc1"), Failover: &structs.DiscoveryFailover{ Definition: &wildFail, Targets: []structs.DiscoveryTarget{ newTarget("main", "", "default", "dc2"), newTarget("main", "", "default", "dc4"), }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "", "default", "dc1"), newTarget("main", "", "default", "dc2"), newTarget("main", "", "default", "dc4"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_NoopSplit_WithDefaultSubset() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") entries.AddSplitters( &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "main", Splits: []structs.ServiceSplit{ {Weight: 100}, }, }, ) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", DefaultSubset: "v2", Subsets: map[string]structs.ServiceResolverSubset{ "v1": {Filter: "Service.Meta.version == 1"}, "v2": {Filter: "Service.Meta.version == 2"}, }, }, ) resolver := entries.GetResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "http", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main", Splits: []*structs.DiscoverySplit{ { Weight: 100, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "v2", "default", "dc1"), }, }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "v2", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "v2", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_DefaultResolver() compileTestCase { entries := newEntries() resolver := newDefaultServiceResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, Default: true, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "", "default", "dc1"), }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect, expectIsDefault: true} } func testcase_DefaultResolver_WithProxyDefaults() compileTestCase { entries := newEntries() entries.GlobalProxy = &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "grpc", }, MeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, }, } resolver := newDefaultServiceResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "grpc", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, Default: true, ConnectTimeout: 5 * time.Second, MeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, }, Target: newTarget("main", "", "default", "dc1"), }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect, expectIsDefault: true} } func testcase_RedirectToDefaultResolverIsNotDefaultChain() compileTestCase { entries := newEntries() entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", }, }, ) resolver := newDefaultServiceResolver("other") expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "other", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, Default: true, ConnectTimeout: 5 * time.Second, Target: newTarget("other", "", "default", "dc1"), }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "other": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("other", "", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("other", "", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect, expectIsDefault: false /*being explicit here because this is the whole point of this test*/} } func testcase_Resolve_WithDefaultSubset() compileTestCase { entries := newEntries() entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", DefaultSubset: "v2", Subsets: map[string]structs.ServiceResolverSubset{ "v1": {Filter: "Service.Meta.version == 1"}, "v2": {Filter: "Service.Meta.version == 2"}, }, }, ) resolver := entries.GetResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "v2", "default", "dc1"), }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "v2", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "v2", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_CircularSplit() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") setServiceProtocol(entries, "other", "http") entries.AddSplitters( &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "main", Splits: []structs.ServiceSplit{ {Weight: 60, Service: "other"}, {Weight: 40, Service: "main"}, // goes straight to resolver }, }, &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "other", Splits: []structs.ServiceSplit{ {Weight: 60, Service: "main"}, {Weight: 40, Service: "other"}, // goes straight to resolver }, }, ) resolveMain := newDefaultServiceResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "http", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main", Splits: []*structs.DiscoverySplit{ { Weight: 60, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolveMain, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "v2", "default", "dc1"), }, }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolveMain, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "v2", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "v2", "default", "dc1"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_MultiDatacenterCanary() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") setServiceProtocol(entries, "main-dc2", "http") setServiceProtocol(entries, "main-dc3", "http") entries.AddSplitters( &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "main", Splits: []structs.ServiceSplit{ {Weight: 60, Service: "main-dc2"}, {Weight: 40, Service: "main-dc3"}, }, }, ) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main-dc2", Redirect: &structs.ServiceResolverRedirect{ Service: "main", Datacenter: "dc2", }, }, &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main-dc3", Redirect: &structs.ServiceResolverRedirect{ Service: "main", Datacenter: "dc3", }, }, &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", ConnectTimeout: 33 * time.Second, }, ) resolver := entries.GetResolver("main") expect := &structs.CompiledDiscoveryChain{ Protocol: "http", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main", Splits: []*structs.DiscoverySplit{ { Weight: 60, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, ConnectTimeout: 33 * time.Second, Target: newTarget("main", "", "default", "dc2"), }, }, }, { Weight: 40, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolver, ConnectTimeout: 33 * time.Second, Target: newTarget("main", "", "default", "dc3"), }, }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolver, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "", "default", "dc2"), newTarget("main", "", "default", "dc3"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "", "default", "dc2"): nil, newTarget("main", "", "default", "dc3"): nil, }, } return compileTestCase{entries: entries, expect: expect} } func testcase_AllBellsAndWhistles() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") setServiceProtocol(entries, "svc-redirect", "http") setServiceProtocol(entries, "svc-redirect-again", "http") setServiceProtocol(entries, "svc-split", "http") setServiceProtocol(entries, "svc-split-again", "http") setServiceProtocol(entries, "svc-split-one-more-time", "http") setServiceProtocol(entries, "redirected", "http") entries.AddRouters( &structs.ServiceRouterConfigEntry{ Kind: "service-router", Name: "main", Routes: []structs.ServiceRoute{ newSimpleRoute("svc-redirect"), // double redirected to a default subset newSimpleRoute("svc-split"), // one split is split further }, }, ) entries.AddSplitters( &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "svc-split", Splits: []structs.ServiceSplit{ {Weight: 60, Service: "svc-redirect"}, // double redirected to a default subset {Weight: 40, Service: "svc-split-again"}, // split again }, }, &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "svc-split-again", Splits: []structs.ServiceSplit{ {Weight: 75, Service: "main", ServiceSubset: "v1"}, {Weight: 25, Service: "svc-split-one-more-time"}, }, }, &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "svc-split-one-more-time", Splits: []structs.ServiceSplit{ {Weight: 80, Service: "main", ServiceSubset: "v2"}, {Weight: 20, Service: "main", ServiceSubset: "v3"}, }, }, ) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "svc-redirect", Redirect: &structs.ServiceResolverRedirect{ Service: "svc-redirect-again", }, }, &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "svc-redirect-again", Redirect: &structs.ServiceResolverRedirect{ Service: "redirected", }, }, &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "redirected", DefaultSubset: "prod", Subsets: map[string]structs.ServiceResolverSubset{ "prod": {Filter: "ServiceMeta.env == prod"}, "qa": {Filter: "ServiceMeta.env == qa"}, }, }, &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", DefaultSubset: "default-subset", Subsets: map[string]structs.ServiceResolverSubset{ "v1": {Filter: "Service.Meta.version == 1"}, "v2": {Filter: "Service.Meta.version == 2"}, "v3": {Filter: "Service.Meta.version == 3"}, "default-subset": {OnlyPassing: true}, }, }, ) var ( router = entries.GetRouter("main") resolverMain = entries.GetResolver("main") resolverRedirected = entries.GetResolver("redirected") ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeRouter, Name: "main", Routes: []*structs.DiscoveryRoute{ { Definition: &router.Routes[0], DestinationNode: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "redirected", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolverRedirected, ConnectTimeout: 5 * time.Second, Target: newTarget("redirected", "prod", "default", "dc1"), }, }, }, { Definition: &router.Routes[1], DestinationNode: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "svc-split", Splits: []*structs.DiscoverySplit{ { Weight: 60, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "redirected", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolverRedirected, ConnectTimeout: 5 * time.Second, Target: newTarget("redirected", "prod", "default", "dc1"), }, }, }, { Weight: 30, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolverMain, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "v1", "default", "dc1"), }, }, }, { Weight: 8, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolverMain, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "v2", "default", "dc1"), }, }, }, { Weight: 2, Node: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolverMain, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "v3", "default", "dc1"), }, }, }, }, }, }, { Definition: newDefaultServiceRoute("main"), DestinationNode: &structs.DiscoveryGraphNode{ Type: structs.DiscoveryGraphNodeTypeGroupResolver, Name: "main", GroupResolver: &structs.DiscoveryGroupResolver{ Definition: resolverMain, ConnectTimeout: 5 * time.Second, Target: newTarget("main", "default-subset", "default", "dc1"), }, }, }, }, }, Resolvers: map[string]*structs.ServiceResolverConfigEntry{ "main": resolverMain, "redirected": resolverRedirected, }, Targets: []structs.DiscoveryTarget{ newTarget("main", "default-subset", "default", "dc1"), newTarget("main", "v1", "default", "dc1"), newTarget("main", "v2", "default", "dc1"), newTarget("main", "v3", "default", "dc1"), newTarget("redirected", "prod", "default", "dc1"), }, GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{ newTarget("main", "default-subset", "default", "dc1"): nil, newTarget("main", "v1", "default", "dc1"): nil, newTarget("main", "v2", "default", "dc1"): nil, newTarget("main", "v3", "default", "dc1"): nil, newTarget("redirected", "prod", "default", "dc1"): nil, }, } 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 { r := structs.ServiceRoute{ Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{PathPrefix: "/" + name}, }, Destination: &structs.ServiceRouteDestination{Service: name}, } for _, mut := range muts { mut(&r) } return r } func setGlobalProxyProtocol(entries *structs.DiscoveryChainConfigEntries, protocol string) { entries.GlobalProxy = &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": protocol, }, } } func setServiceProtocol(entries *structs.DiscoveryChainConfigEntries, name, protocol string) { entries.AddServices(&structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: name, Protocol: protocol, }) } func newEntries() *structs.DiscoveryChainConfigEntries { return &structs.DiscoveryChainConfigEntries{ Routers: make(map[string]*structs.ServiceRouterConfigEntry), Splitters: make(map[string]*structs.ServiceSplitterConfigEntry), Resolvers: make(map[string]*structs.ServiceResolverConfigEntry), } } func newTarget(service, serviceSubset, namespace, datacenter string) structs.DiscoveryTarget { return structs.DiscoveryTarget{ Service: service, ServiceSubset: serviceSubset, Namespace: namespace, Datacenter: datacenter, } }