// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package discoverychain import ( "testing" "time" "github.com/stretchr/testify/require" "github.com/hashicorp/consul/agent/configentry" "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/proto/private/pbcommon" "github.com/hashicorp/consul/proto/private/pbpeering" ) type compileTestCase struct { entries *configentry.DiscoveryChainSet setup func(req *CompileRequest) expect *structs.CompiledDiscoveryChain expectCustom bool expectErr string expectGraphErr bool } func TestCompile(t *testing.T) { t.Parallel() 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(), "router with no destination": testcase_JustRouterWithNoDestination(), "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(), "redirect to cluster peer": testcase_PeerRedirect(), "redirect to cluster peer http proxy-defaults": testcase_PeerRedirectProxyDefHTTP(), "redirect to cluster peer http service-defaults": testcase_PeerRedirectSvcDefHTTP(), "datacenter redirect with mesh gateways": testcase_DatacenterRedirect_WithMeshGateways(), "service failover": testcase_ServiceFailover(), "service failover through redirect": testcase_ServiceFailoverThroughRedirect(), "circular resolver failover": testcase_Resolver_CircularFailover(), "service and subset failover": testcase_ServiceAndSubsetFailover(), "datacenter failover": testcase_DatacenterFailover(), "datacenter failover with mesh gateways": testcase_DatacenterFailover_WithMeshGateways(), "target failover": testcase_Failover_Targets(), "noop split to resolver with default subset": testcase_NoopSplit_WithDefaultSubset(), "resolver with default subset": testcase_Resolve_WithDefaultSubset(), "default resolver with external sni": testcase_DefaultResolver_ExternalSNI(), "resolver with no entries and inferring defaults": testcase_DefaultResolver(), "default resolver with proxy defaults": testcase_DefaultResolver_WithProxyDefaults(), "loadbalancer splitter and resolver": testcase_LBSplitterAndResolver(), "loadbalancer resolver": testcase_LBResolver(), "service redirect to service with default resolver is not a default chain": testcase_RedirectToDefaultResolverIsNotDefaultChain(), "extensions": testcase_Extensions(), "service meta projection": testcase_ServiceMetaProjection(), "service meta projection with redirect": testcase_ServiceMetaProjectionWithRedirect(), "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(), "resolver with failover and external sni": testcase_Resolver_ExternalSNI_FailoverNotAllowed(), "resolver with subsets and external sni": testcase_Resolver_ExternalSNI_SubsetsNotAllowed(), "resolver with redirect and external sni": testcase_Resolver_ExternalSNI_RedirectNotAllowed(), // overrides "resolver with protocol from override": testcase_ResolverProtocolOverride(), "resolver with protocol from override ignored": testcase_ResolverProtocolOverrideIgnored(), "router ignored due to protocol override": testcase_RouterIgnored_ResolverProtocolOverride(), // circular references "circular resolver redirect": testcase_Resolver_CircularRedirect(), "circular split": testcase_CircularSplit(), // tproxy "tproxy service defaults only": testcase_ServiceDefaultsTProxy(), "tproxy proxy defaults only": testcase_ProxyDefaultsTProxy(), "tproxy service defaults override": testcase_ServiceDefaultsOverrideTProxy(), } 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()) } req := CompileRequest{ ServiceName: "main", EvaluateInNamespace: "default", EvaluateInPartition: "default", EvaluateInDatacenter: "dc1", EvaluateInTrustDomain: "trustdomain.consul", Entries: tc.entries, } if tc.setup != nil { tc.setup(&req) } res, err := Compile(req) 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.Partition = "default" tc.expect.Datacenter = "dc1" if tc.expectCustom { require.NotEmpty(t, res.CustomizationHash) res.CustomizationHash = "" } else { require.Empty(t, res.CustomizationHash) } require.Equal(t, tc.expect, res) } }) } } func testcase_JustRouterWithDefaults() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") entries.AddRouters( &structs.ServiceRouterConfigEntry{ Kind: "service-router", Name: "main", }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { Definition: newDefaultServiceRoute("main", "default", "default"), NextNode: "resolver:main.default.default.dc1", }, }, }, "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_JustRouterWithNoDestination() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") entries.AddRouters( &structs.ServiceRouterConfigEntry{ Kind: "service-router", Name: "main", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathPrefix: "/", }, }, }, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { Definition: &structs.ServiceRoute{ Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathPrefix: "/", }, }, }, NextNode: "resolver:main.default.default.dc1", }, { Definition: newDefaultServiceRoute("main", "default", "default"), NextNode: "resolver:main.default.default.dc1", }, }, }, "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, 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, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { Definition: newDefaultServiceRoute("main", "default", "default"), NextNode: "resolver:main.default.default.dc1", }, }, }, "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 33 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": targetWithConnectTimeout( newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), 33*time.Second, ), }, } 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}, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { Definition: newDefaultServiceRoute("main", "default", "default"), NextNode: "splitter:main.default.default", }, }, }, "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ Weight: 100, }, Weight: 100, NextNode: "resolver:main.default.default.dc1", }, }, }, "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, 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}, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { Definition: newDefaultServiceRoute("main", "default", "default"), NextNode: "splitter:main.default.default", }, }, }, "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ Weight: 100, }, Weight: 100, NextNode: "resolver:main.default.default.dc1", }, }, }, "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ Service: "main", Datacenter: "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, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { Definition: newDefaultServiceRoute("main", "default", "default"), NextNode: "splitter:main.default.default", }, }, }, "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ Weight: 100, }, Weight: 100, NextNode: "resolver:main.default.default.dc1", }, }, }, "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 33 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": targetWithConnectTimeout( newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), 33*time.Second, ), }, } 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(structs.NewServiceID("main", nil)) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { Definition: &router.Routes[0], NextNode: "resolver:bypass.other.default.default.dc1", }, { Definition: newDefaultServiceRoute("main", "default", "default"), NextNode: "resolver:main.default.default.dc1", }, }, }, "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, "resolver:bypass.other.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "bypass.other.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "bypass.other.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), "bypass.other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ Service: "other", ServiceSubset: "bypass", }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == bypass", } }), }, } 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}, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ Weight: 100, }, Weight: 100, NextNode: "resolver:main.default.default.dc1", }, }, }, "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, 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, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ Weight: 100, }, Weight: 100, NextNode: "resolver:main.default.default.dc1", }, }, }, "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 33 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": targetWithConnectTimeout( newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), 33*time.Second, ), }, } 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", }, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ Weight: 60, ServiceSubset: "v2", }, Weight: 60, NextNode: "resolver:v2.main.default.default.dc1", }, { Definition: &structs.ServiceSplit{ Weight: 40, ServiceSubset: "v1", }, Weight: 40, NextNode: "resolver:v1.main.default.default.dc1", }, }, }, "resolver:v2.main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "v2.main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "v2.main.default.default.dc1", }, }, "resolver:v1.main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "v1.main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "v1.main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ Service: "main", ServiceSubset: "v2", }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 2", } }), "v1.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ Service: "main", ServiceSubset: "v1", }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 1", } }), }, } 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"}, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ Weight: 60, Service: "foo", }, Weight: 60, NextNode: "resolver:foo.default.default.dc1", }, { Definition: &structs.ServiceSplit{ Weight: 40, Service: "bar", }, Weight: 40, NextNode: "resolver:bar.default.default.dc1", }, }, }, "resolver:foo.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "foo.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "foo.default.default.dc1", }, }, "resolver:bar.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "bar.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "bar.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "foo.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "foo"}, nil), "bar.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "bar"}, 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", }, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ Weight: 100, Service: "next", ServiceSubset: "bypassed", }, Weight: 100, NextNode: "resolver:bypassed.next.default.default.dc1", }, }, }, "resolver:bypassed.next.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "bypassed.next.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "bypassed.next.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "bypassed.next.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ Service: "next", ServiceSubset: "bypassed", }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == bypass", } }), }, } 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", }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", StartNode: "resolver:other.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:other.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "other.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "other.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "other"}, 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", }, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", StartNode: "resolver:v2.other.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:v2.other.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "v2.other.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "v2.other.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "v2.other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ Service: "other", ServiceSubset: "v2", }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 2", } }), }, } 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", }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", StartNode: "resolver:main.default.default.dc9", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc9": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc9", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc9", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc9": newTarget(structs.DiscoveryTargetOpts{ Service: "main", Datacenter: "dc9", }, nil), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_PeerRedirect() compileTestCase { entries := newEntries() entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", Peer: "cluster-01", }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", StartNode: "resolver:other.default.default.external.cluster-01", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:other.default.default.external.cluster-01": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "other.default.default.external.cluster-01", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "other.default.default.external.cluster-01", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "other.default.default.external.cluster-01": newTarget(structs.DiscoveryTargetOpts{ Service: "other", Peer: "cluster-01", }, func(t *structs.DiscoveryTarget) { t.SNI = "" t.Name = "" t.Datacenter = "" }), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_PeerRedirectProxyDefHTTP() compileTestCase { entries := newEntries() entries.AddProxyDefaults(&structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "Protocol": "http", }, }) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", Peer: "cluster-01", }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "resolver:other.default.default.external.cluster-01", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:other.default.default.external.cluster-01": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "other.default.default.external.cluster-01", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "other.default.default.external.cluster-01", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "other.default.default.external.cluster-01": newTarget(structs.DiscoveryTargetOpts{ Service: "other", Peer: "cluster-01", }, func(t *structs.DiscoveryTarget) { t.SNI = "" t.Name = "" t.Datacenter = "" }), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_PeerRedirectSvcDefHTTP() compileTestCase { entries := newEntries() entries.AddServices( &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, ) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", Peer: "cluster-01", }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "resolver:other.default.default.external.cluster-01", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:other.default.default.external.cluster-01": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "other.default.default.external.cluster-01", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "other.default.default.external.cluster-01", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "other.default.default.external.cluster-01": newTarget(structs.DiscoveryTargetOpts{ Service: "other", Peer: "cluster-01", }, func(t *structs.DiscoveryTarget) { t.SNI = "" t.Name = "" t.Datacenter = "" }), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_DatacenterRedirect_WithMeshGateways() compileTestCase { entries := newEntries() entries.AddProxyDefaults(&structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, MeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, }, }) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Redirect: &structs.ServiceResolverRedirect{ Datacenter: "dc9", }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", StartNode: "resolver:main.default.default.dc9", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc9": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc9", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc9", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc9": newTarget(structs.DiscoveryTargetOpts{ Service: "main", Datacenter: "dc9", }, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } }), }, } 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"}, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", Failover: &structs.DiscoveryFailover{ Targets: []string{"backup.default.default.dc1"}, }, }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), "backup.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "backup"}, nil), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_ServiceFailoverThroughRedirect() compileTestCase { entries := newEntries() entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "backup", Redirect: &structs.ServiceResolverRedirect{ Service: "actual", }, }, &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Failover: map[string]structs.ServiceResolverFailover{ "*": {Service: "backup"}, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", Failover: &structs.DiscoveryFailover{ Targets: []string{"actual.default.default.dc1"}, }, }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), "actual.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "actual"}, nil), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_Resolver_CircularFailover() compileTestCase { entries := newEntries() entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "backup", Failover: map[string]structs.ServiceResolverFailover{ "*": {Service: "main"}, }, }, &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Failover: map[string]structs.ServiceResolverFailover{ "*": {Service: "backup"}, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", Failover: &structs.DiscoveryFailover{ Targets: []string{"backup.default.default.dc1"}, }, }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), "backup.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "backup"}, 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"}, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", Failover: &structs.DiscoveryFailover{ Targets: []string{"backup.main.default.default.dc1"}, }, }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), "backup.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ Service: "main", ServiceSubset: "backup", }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == backup", } }), }, } 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"}}, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", Failover: &structs.DiscoveryFailover{ Targets: []string{"main.default.default.dc2", "main.default.default.dc4"}, }, }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), "main.default.default.dc2": newTarget(structs.DiscoveryTargetOpts{ Service: "main", Datacenter: "dc2", }, nil), "main.default.default.dc4": newTarget(structs.DiscoveryTargetOpts{ Service: "main", Datacenter: "dc4", }, nil), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_DatacenterFailover_WithMeshGateways() compileTestCase { entries := newEntries() entries.AddProxyDefaults(&structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, MeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, }, }) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Failover: map[string]structs.ServiceResolverFailover{ "*": {Datacenters: []string{"dc2", "dc4"}}, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", Failover: &structs.DiscoveryFailover{ Targets: []string{ "main.default.default.dc2", "main.default.default.dc4", }, }, }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } }), "main.default.default.dc2": newTarget(structs.DiscoveryTargetOpts{ Service: "main", Datacenter: "dc2", }, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } }), "main.default.default.dc4": newTarget(structs.DiscoveryTargetOpts{ Service: "main", Datacenter: "dc4", }, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } }), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_Failover_Targets() compileTestCase { entries := newEntries() entries.AddProxyDefaults(&structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, MeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, }, }) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Failover: map[string]structs.ServiceResolverFailover{ "*": { Targets: []structs.ServiceResolverFailoverTarget{ {Datacenter: "dc3"}, {Service: "new-main"}, {Peer: "cluster-01"}, {Peer: "cluster-02"}, }, }, }, }, ) entries.AddPeers( &pbpeering.Peering{ Name: "cluster-01", Remote: &pbpeering.RemoteInfo{ Locality: &pbcommon.Locality{ Region: "us-west-1", Zone: "us-west-1a", }, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", Failover: &structs.DiscoveryFailover{ Targets: []string{ "main.default.default.dc3", "new-main.default.default.dc1", "main.default.default.external.cluster-01", "main.default.default.external.cluster-02", }, }, }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } }), "main.default.default.dc3": newTarget(structs.DiscoveryTargetOpts{ Service: "main", Datacenter: "dc3", }, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } }), "new-main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "new-main"}, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } }), "main.default.default.external.cluster-01": newTarget(structs.DiscoveryTargetOpts{ Service: "main", Peer: "cluster-01", }, func(t *structs.DiscoveryTarget) { t.SNI = "" t.Name = "" t.Datacenter = "" t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } t.Locality = &structs.Locality{ Region: "us-west-1", Zone: "us-west-1a", } }), "main.default.default.external.cluster-02": newTarget(structs.DiscoveryTargetOpts{ Service: "main", Peer: "cluster-02", }, func(t *structs.DiscoveryTarget) { t.SNI = "" t.Name = "" t.Datacenter = "" t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } }), }, } 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"}, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ Weight: 100, }, Weight: 100, NextNode: "resolver:v2.main.default.default.dc1", }, }, }, "resolver:v2.main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "v2.main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "v2.main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ Service: "main", ServiceSubset: "v2", }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 2", } }), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_DefaultResolver() compileTestCase { entries := newEntries() expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Default: true, StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ // TODO-TARGET "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_DefaultResolver_WithProxyDefaults() compileTestCase { entries := newEntries() entries.AddProxyDefaults(&structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "grpc", }, MeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, }, }) expect := &structs.CompiledDiscoveryChain{ Protocol: "grpc", Default: true, StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } }), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_Extensions() compileTestCase { entries := newEntries() entries.AddProxyDefaults(&structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, EnvoyExtensions: []structs.EnvoyExtension{ { Name: "ext1", }, }, }) entries.AddServices( &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", EnvoyExtensions: []structs.EnvoyExtension{ { Name: "ext2", }, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Default: true, EnvoyExtensions: []structs.EnvoyExtension{ { Name: "ext1", }, { Name: "ext2", }, }, StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_ServiceMetaProjection() compileTestCase { entries := newEntries() entries.AddServices( &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Meta: map[string]string{ "foo": "bar", "abc": "123", }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Default: true, ServiceMeta: map[string]string{ "foo": "bar", "abc": "123", }, StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_ServiceMetaProjectionWithRedirect() compileTestCase { entries := newEntries() entries.AddServices( &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Meta: map[string]string{ "foo": "bar", "abc": "123", }, }, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "other", Meta: map[string]string{ "zim": "gir", "abc": "456", "xyz": "999", }, }, ) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", ServiceMeta: map[string]string{ // Note this is main's meta, not other's. "foo": "bar", "abc": "123", }, StartNode: "resolver:other.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:other.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "other.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "other.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "other"}, nil), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_RedirectToDefaultResolverIsNotDefaultChain() compileTestCase { entries := newEntries() entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", StartNode: "resolver:other.default.default.dc1", Default: false, /*being explicit here because this is the whole point of this test*/ Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:other.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "other.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "other.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "other"}, nil), }, } return compileTestCase{entries: entries, expect: expect} } 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"}, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", StartNode: "resolver:v2.main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:v2.main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "v2.main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "v2.main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ Service: "main", ServiceSubset: "v2", }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 2", } }), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_DefaultResolver_ExternalSNI() compileTestCase { entries := newEntries() entries.AddServices(&structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", ExternalSNI: "main.some.other.service.mesh", }) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Default: true, StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) { t.SNI = "main.some.other.service.mesh" t.External = true }), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_Resolver_ExternalSNI_FailoverNotAllowed() compileTestCase { entries := newEntries() entries.AddServices(&structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", ExternalSNI: "main.some.other.service.mesh", }) entries.AddResolvers(&structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", ConnectTimeout: 33 * time.Second, Failover: map[string]structs.ServiceResolverFailover{ "*": {Service: "backup"}, }, }) return compileTestCase{ entries: entries, expectErr: `service "main" has an external SNI set; cannot define failover for external services`, expectGraphErr: true, } } func testcase_Resolver_ExternalSNI_SubsetsNotAllowed() compileTestCase { entries := newEntries() entries.AddServices(&structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", ExternalSNI: "main.some.other.service.mesh", }) entries.AddResolvers(&structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", ConnectTimeout: 33 * time.Second, Subsets: map[string]structs.ServiceResolverSubset{ "canary": { Filter: "Service.Meta.version == canary", }, }, }) return compileTestCase{ entries: entries, expectErr: `service "main" has an external SNI set; cannot define subsets for external services`, expectGraphErr: true, } } func testcase_Resolver_ExternalSNI_RedirectNotAllowed() compileTestCase { entries := newEntries() entries.AddServices(&structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", ExternalSNI: "main.some.other.service.mesh", }) entries.AddResolvers(&structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", ConnectTimeout: 33 * time.Second, Redirect: &structs.ServiceResolverRedirect{ Datacenter: "dc2", }, }) return compileTestCase{ entries: entries, expectErr: `service "main" has an external SNI set; cannot define redirects for external services`, expectGraphErr: true, } } 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, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ Weight: 60, Service: "main-dc2", }, Weight: 60, NextNode: "resolver:main.default.default.dc2", }, { Definition: &structs.ServiceSplit{ Weight: 40, Service: "main-dc3", }, Weight: 40, NextNode: "resolver:main.default.default.dc3", }, }, }, "resolver:main.default.default.dc2": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc2", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 33 * time.Second, Target: "main.default.default.dc2", }, }, "resolver:main.default.default.dc3": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc3", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 33 * time.Second, Target: "main.default.default.dc3", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc2": targetWithConnectTimeout( newTarget(structs.DiscoveryTargetOpts{ Service: "main", Datacenter: "dc2", }, nil), 33*time.Second, ), "main.default.default.dc3": targetWithConnectTimeout( newTarget(structs.DiscoveryTargetOpts{ Service: "main", Datacenter: "dc3", }, nil), 33*time.Second, ), }, } 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", RequestHeaders: &structs.HTTPHeaderModifiers{ Set: map[string]string{ "parent": "1", "shared": "from-parent", }, }, ResponseHeaders: &structs.HTTPHeaderModifiers{ Set: map[string]string{ "parent": "2", "shared": "from-parent", }, }, }, }, }, &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", RequestHeaders: &structs.HTTPHeaderModifiers{ Set: map[string]string{ "child": "3", "shared": "from-child", }, }, ResponseHeaders: &structs.HTTPHeaderModifiers{ Set: map[string]string{ "child": "4", "shared": "from-parent", }, }, }, }, }, ) 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"}, }, LoadBalancer: &structs.LoadBalancer{ Policy: "ring_hash", RingHashConfig: &structs.RingHashConfig{ MaximumRingSize: 100, }, HashPolicies: []structs.HashPolicy{ { SourceIP: true, }, }, }, }, &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(structs.NewServiceID("main", nil)) ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { Definition: &router.Routes[0], NextNode: "resolver:prod.redirected.default.default.dc1", }, { Definition: &router.Routes[1], NextNode: "splitter:svc-split.default.default", }, { Definition: newDefaultServiceRoute("main", "default", "default"), NextNode: "resolver:default-subset.main.default.default.dc1", }, }, }, "splitter:svc-split.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "svc-split.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ Weight: 60, Service: "svc-redirect", }, Weight: 60, NextNode: "resolver:prod.redirected.default.default.dc1", }, { Definition: &structs.ServiceSplit{ Weight: 75, Service: "main", ServiceSubset: "v1", }, Weight: 30, NextNode: "resolver:v1.main.default.default.dc1", }, { Definition: &structs.ServiceSplit{ Weight: 80, Service: "main", ServiceSubset: "v2", // Should inherit these from parent verbatim as there was no // child-split header manip. RequestHeaders: &structs.HTTPHeaderModifiers{ Set: map[string]string{ "parent": "1", "shared": "from-parent", }, }, ResponseHeaders: &structs.HTTPHeaderModifiers{ Set: map[string]string{ "parent": "2", "shared": "from-parent", }, }, }, Weight: 8, NextNode: "resolver:v2.main.default.default.dc1", }, { Definition: &structs.ServiceSplit{ Weight: 20, Service: "main", ServiceSubset: "v3", // Should get a merge of child and parent rules RequestHeaders: &structs.HTTPHeaderModifiers{ Set: map[string]string{ "parent": "1", "child": "3", "shared": "from-child", }, }, ResponseHeaders: &structs.HTTPHeaderModifiers{ Set: map[string]string{ "parent": "2", "child": "4", "shared": "from-parent", }, }, }, Weight: 2, NextNode: "resolver:v3.main.default.default.dc1", }, }, LoadBalancer: &structs.LoadBalancer{ Policy: "ring_hash", RingHashConfig: &structs.RingHashConfig{ MaximumRingSize: 100, }, HashPolicies: []structs.HashPolicy{ { SourceIP: true, }, }, }, }, "resolver:prod.redirected.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "prod.redirected.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "prod.redirected.default.default.dc1", }, LoadBalancer: &structs.LoadBalancer{ Policy: "ring_hash", RingHashConfig: &structs.RingHashConfig{ MaximumRingSize: 100, }, HashPolicies: []structs.HashPolicy{ { SourceIP: true, }, }, }, }, "resolver:v1.main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "v1.main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "v1.main.default.default.dc1", }, }, "resolver:v2.main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "v2.main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "v2.main.default.default.dc1", }, }, "resolver:v3.main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "v3.main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "v3.main.default.default.dc1", }, }, "resolver:default-subset.main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "default-subset.main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ ConnectTimeout: 5 * time.Second, Target: "default-subset.main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "prod.redirected.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ Service: "redirected", ServiceSubset: "prod", }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "ServiceMeta.env == prod", } }), "v1.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ Service: "main", ServiceSubset: "v1", }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 1", } }), "v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ Service: "main", ServiceSubset: "v2", }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 2", } }), "v3.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ Service: "main", ServiceSubset: "v3", }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 3", } }), "default-subset.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ Service: "main", ServiceSubset: "default-subset", }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{OnlyPassing: true} }), }, } 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{ "*": { 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 testcase_ResolverProtocolOverride() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "grpc") expect := &structs.CompiledDiscoveryChain{ Protocol: "http2", Default: true, StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ // TODO-TARGET "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect, expectCustom: true, setup: func(req *CompileRequest) { req.OverrideProtocol = "http2" }, } } func testcase_ResolverProtocolOverrideIgnored() compileTestCase { // This shows that if you try to override the protocol to its current value // the override is completely ignored. entries := newEntries() setServiceProtocol(entries, "main", "http2") expect := &structs.CompiledDiscoveryChain{ Protocol: "http2", Default: true, StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ // TODO-TARGET "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect, setup: func(req *CompileRequest) { req.OverrideProtocol = "http2" }, } } func testcase_RouterIgnored_ResolverProtocolOverride() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "grpc") entries.AddRouters( &structs.ServiceRouterConfigEntry{ Kind: "service-router", Name: "main", }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", StartNode: "resolver:main.default.default.dc1", Default: true, Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ // TODO-TARGET "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect, expectCustom: true, setup: func(req *CompileRequest) { req.OverrideProtocol = "tcp" }, } } func testcase_Resolver_CircularRedirect() compileTestCase { entries := newEntries() entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", }, }, &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "other", Redirect: &structs.ServiceResolverRedirect{ Service: "main", }, }, ) return compileTestCase{entries: entries, expectErr: "detected circular resolver redirect", expectGraphErr: true, } } func testcase_CircularSplit() compileTestCase { entries := newEntries() setGlobalProxyProtocol(entries, "http") entries.AddSplitters( &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "main", Splits: []structs.ServiceSplit{ {Weight: 100, Service: "other"}, }, }, &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "other", Splits: []structs.ServiceSplit{ {Weight: 100, Service: "main"}, }, }, ) return compileTestCase{entries: entries, expectErr: "detected circular reference", expectGraphErr: true, } } func testcase_LBSplitterAndResolver() compileTestCase { entries := newEntries() setServiceProtocol(entries, "foo", "http") setServiceProtocol(entries, "bar", "http") setServiceProtocol(entries, "baz", "http") entries.AddSplitters( &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "main", Splits: []structs.ServiceSplit{ {Weight: 60, Service: "foo"}, {Weight: 20, Service: "bar"}, {Weight: 20, Service: "baz"}, }, }, ) entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "foo", LoadBalancer: &structs.LoadBalancer{ Policy: "least_request", LeastRequestConfig: &structs.LeastRequestConfig{ ChoiceCount: 3, }, }, }, &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "bar", LoadBalancer: &structs.LoadBalancer{ Policy: "ring_hash", RingHashConfig: &structs.RingHashConfig{ MaximumRingSize: 101, }, HashPolicies: []structs.HashPolicy{ { SourceIP: true, }, }, }, }, &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "baz", LoadBalancer: &structs.LoadBalancer{ Policy: "maglev", HashPolicies: []structs.HashPolicy{ { Field: "cookie", FieldValue: "chocolate-chip", CookieConfig: &structs.CookieConfig{ TTL: 2 * time.Minute, Path: "/bowl", }, Terminal: true, }, }, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ Weight: 60, Service: "foo", }, Weight: 60, NextNode: "resolver:foo.default.default.dc1", }, { Definition: &structs.ServiceSplit{ Weight: 20, Service: "bar", }, Weight: 20, NextNode: "resolver:bar.default.default.dc1", }, { Definition: &structs.ServiceSplit{ Weight: 20, Service: "baz", }, Weight: 20, NextNode: "resolver:baz.default.default.dc1", }, }, // The LB config from bar is attached because splitters only care about hash-based policies, // and it's the config from bar not baz because we pick the first one we encounter in the Splits. LoadBalancer: &structs.LoadBalancer{ Policy: "ring_hash", RingHashConfig: &structs.RingHashConfig{ MaximumRingSize: 101, }, HashPolicies: []structs.HashPolicy{ { SourceIP: true, }, }, }, }, // Each service's LB config is passed down from the service-resolver to the resolver node "resolver:foo.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "foo.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: false, ConnectTimeout: 5 * time.Second, Target: "foo.default.default.dc1", }, LoadBalancer: &structs.LoadBalancer{ Policy: "least_request", LeastRequestConfig: &structs.LeastRequestConfig{ ChoiceCount: 3, }, }, }, "resolver:bar.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "bar.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: false, ConnectTimeout: 5 * time.Second, Target: "bar.default.default.dc1", }, LoadBalancer: &structs.LoadBalancer{ Policy: "ring_hash", RingHashConfig: &structs.RingHashConfig{ MaximumRingSize: 101, }, HashPolicies: []structs.HashPolicy{ { SourceIP: true, }, }, }, }, "resolver:baz.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "baz.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: false, ConnectTimeout: 5 * time.Second, Target: "baz.default.default.dc1", }, LoadBalancer: &structs.LoadBalancer{ Policy: "maglev", HashPolicies: []structs.HashPolicy{ { Field: "cookie", FieldValue: "chocolate-chip", CookieConfig: &structs.CookieConfig{ TTL: 2 * time.Minute, Path: "/bowl", }, Terminal: true, }, }, }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "foo.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "foo"}, nil), "bar.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "bar"}, nil), "baz.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "baz"}, nil), }, } return compileTestCase{entries: entries, expect: expect} } // ensure chain with LB cfg in resolver isn't a default chain (!IsDefault) func testcase_LBResolver() compileTestCase { entries := newEntries() setServiceProtocol(entries, "main", "http") entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", LoadBalancer: &structs.LoadBalancer{ Policy: "ring_hash", RingHashConfig: &structs.RingHashConfig{ MaximumRingSize: 101, }, HashPolicies: []structs.HashPolicy{ { SourceIP: true, }, }, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "http", StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: false, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, LoadBalancer: &structs.LoadBalancer{ Policy: "ring_hash", RingHashConfig: &structs.RingHashConfig{ MaximumRingSize: 101, }, HashPolicies: []structs.HashPolicy{ { SourceIP: true, }, }, }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_ServiceDefaultsTProxy() compileTestCase { entries := newEntries() entries.AddServices( &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", TransparentProxy: structs.TransparentProxyConfig{ DialedDirectly: true, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Default: true, StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) { t.TransparentProxy.DialedDirectly = true }), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_ProxyDefaultsTProxy() compileTestCase { entries := newEntries() entries.AddProxyDefaults(&structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, TransparentProxy: structs.TransparentProxyConfig{ DialedDirectly: true, }, }) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Default: true, StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) { t.TransparentProxy.DialedDirectly = true }), }, } return compileTestCase{entries: entries, expect: expect} } func testcase_ServiceDefaultsOverrideTProxy() compileTestCase { entries := newEntries() entries.AddProxyDefaults(&structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, TransparentProxy: structs.TransparentProxyConfig{ DialedDirectly: false, }, }) entries.AddServices( &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", TransparentProxy: structs.TransparentProxyConfig{ DialedDirectly: true, }, }, ) expect := &structs.CompiledDiscoveryChain{ Protocol: "tcp", Default: true, StartNode: "resolver:main.default.default.dc1", Nodes: map[string]*structs.DiscoveryGraphNode{ "resolver:main.default.default.dc1": { Type: structs.DiscoveryGraphNodeTypeResolver, Name: "main.default.default.dc1", Resolver: &structs.DiscoveryResolver{ Default: true, ConnectTimeout: 5 * time.Second, Target: "main.default.default.dc1", }, }, }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) { t.TransparentProxy.DialedDirectly = true }), }, } return compileTestCase{entries: entries, expect: expect} } 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 *configentry.DiscoveryChainSet, protocol string) { entries.AddProxyDefaults(&structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": protocol, }, }) } func setServiceProtocol(entries *configentry.DiscoveryChainSet, name, protocol string) { entries.AddServices(&structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: name, Protocol: protocol, }) } func newEntries() *configentry.DiscoveryChainSet { return &configentry.DiscoveryChainSet{ Routers: make(map[structs.ServiceID]*structs.ServiceRouterConfigEntry), Splitters: make(map[structs.ServiceID]*structs.ServiceSplitterConfigEntry), Resolvers: make(map[structs.ServiceID]*structs.ServiceResolverConfigEntry), } } func newTarget(opts structs.DiscoveryTargetOpts, modFn func(t *structs.DiscoveryTarget)) *structs.DiscoveryTarget { if opts.Namespace == "" { opts.Namespace = "default" } if opts.Partition == "" { opts.Partition = "default" } if opts.Datacenter == "" { opts.Datacenter = "dc1" } t := structs.NewDiscoveryTarget(opts) t.SNI = connect.TargetSNI(t, "trustdomain.consul") t.Name = t.SNI t.ConnectTimeout = 5 * time.Second // default if modFn != nil { modFn(t) } return t } func targetWithConnectTimeout(t *structs.DiscoveryTarget, connectTimeout time.Duration) *structs.DiscoveryTarget { t.ConnectTimeout = connectTimeout return t }