From 5c1f7aa372582c48fdeddb16b9c35dfa1800b1ce Mon Sep 17 00:00:00 2001 From: freddygv Date: Fri, 12 Nov 2021 18:57:05 -0700 Subject: [PATCH] Allow cross-partition references in disco chain * Add partition fields to targets like service route destinations * Update validation to prevent cross-DC + cross-partition references * Handle partitions when reading config entries for disco chain * Encode partition in compiled targets --- agent/config/runtime_test.go | 2 + agent/consul/discoverychain/compile.go | 26 +-- agent/consul/discoverychain/compile_test.go | 156 +++++++++--------- agent/consul/state/config_entry.go | 46 ++++-- agent/consul/state/config_entry_test.go | 7 + agent/structs/config_entry_discoverychain.go | 97 ++++++++--- .../config_entry_discoverychain_test.go | 2 +- test/integration/connect/envoy/run-tests.sh | 16 ++ 8 files changed, 228 insertions(+), 124 deletions(-) diff --git a/agent/config/runtime_test.go b/agent/config/runtime_test.go index 8abbcc4033..e92fb9855c 100644 --- a/agent/config/runtime_test.go +++ b/agent/config/runtime_test.go @@ -23,6 +23,7 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/cache" "github.com/hashicorp/consul/agent/checks" "github.com/hashicorp/consul/agent/consul" @@ -4085,6 +4086,7 @@ func TestLoad_IntegrationWithFlags(t *testing.T) { Service: "carrot", ServiceSubset: "kale", Namespace: "leek", + Partition: acl.DefaultPartitionName, PrefixRewrite: "/alternate", RequestTimeout: 99 * time.Second, NumRetries: 12345, diff --git a/agent/consul/discoverychain/compile.go b/agent/consul/discoverychain/compile.go index 99f4c357d0..c795ef88c5 100644 --- a/agent/consul/discoverychain/compile.go +++ b/agent/consul/discoverychain/compile.go @@ -185,7 +185,7 @@ type customizationMarkers struct { // the String() method on the type itself. It is this way to be more // consistent with other string ids within the discovery chain. func serviceIDString(sid structs.ServiceID) string { - return fmt.Sprintf("%s.%s", sid.ID, sid.NamespaceOrDefault()) + return fmt.Sprintf("%s.%s.%s", sid.ID, sid.NamespaceOrDefault(), sid.PartitionOrDefault()) } func (m *customizationMarkers) IsZero() bool { @@ -213,10 +213,10 @@ func (c *compiler) recordServiceProtocol(sid structs.ServiceID) error { if serviceDefault := c.entries.GetService(sid); serviceDefault != nil { return c.recordProtocol(sid, serviceDefault.Protocol) } - if c.entries.GlobalProxy != nil { + if proxyDefault := c.entries.GetProxyDefaults(sid.PartitionOrDefault()); proxyDefault != nil { var cfg proxyConfig // Ignore errors and fallback on defaults if it does happen. - _ = mapstructure.WeakDecode(c.entries.GlobalProxy.Config, &cfg) + _ = mapstructure.WeakDecode(proxyDefault.Config, &cfg) if cfg.Protocol != "" { return c.recordProtocol(sid, cfg.Protocol) } @@ -567,11 +567,12 @@ func (c *compiler) assembleChain() error { dest = &structs.ServiceRouteDestination{ Service: c.serviceName, Namespace: router.NamespaceOrDefault(), + Partition: router.PartitionOrDefault(), } } svc := defaultIfEmpty(dest.Service, c.serviceName) destNamespace := defaultIfEmpty(dest.Namespace, router.NamespaceOrDefault()) - destPartition := router.PartitionOrDefault() + destPartition := defaultIfEmpty(dest.Partition, router.PartitionOrDefault()) // Check to see if the destination is eligible for splitting. var ( @@ -602,7 +603,7 @@ func (c *compiler) assembleChain() error { } defaultRoute := &structs.DiscoveryRoute{ - Definition: newDefaultServiceRoute(router.Name, router.NamespaceOrDefault()), + Definition: newDefaultServiceRoute(router.Name, router.NamespaceOrDefault(), router.PartitionOrDefault()), NextNode: defaultDestinationNode.MapKey(), } routeNode.Routes = append(routeNode.Routes, defaultRoute) @@ -613,7 +614,7 @@ func (c *compiler) assembleChain() error { return nil } -func newDefaultServiceRoute(serviceName string, namespace string) *structs.ServiceRoute { +func newDefaultServiceRoute(serviceName, namespace, partition string) *structs.ServiceRoute { return &structs.ServiceRoute{ Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ @@ -623,6 +624,7 @@ func newDefaultServiceRoute(serviceName string, namespace string) *structs.Servi Destination: &structs.ServiceRouteDestination{ Service: serviceName, Namespace: namespace, + Partition: partition, }, } } @@ -836,7 +838,7 @@ RESOLVE_AGAIN: target, redirect.Service, redirect.ServiceSubset, - target.Partition, + redirect.Partition, redirect.Namespace, redirect.Datacenter, ) @@ -940,9 +942,9 @@ RESOLVE_AGAIN: if serviceDefault := c.entries.GetService(targetID); serviceDefault != nil { target.MeshGateway = serviceDefault.MeshGateway } - - if c.entries.GlobalProxy != nil && target.MeshGateway.Mode == structs.MeshGatewayModeDefault { - target.MeshGateway.Mode = c.entries.GlobalProxy.MeshGateway.Mode + proxyDefault := c.entries.GetProxyDefaults(targetID.PartitionOrDefault()) + if proxyDefault != nil && target.MeshGateway.Mode == structs.MeshGatewayModeDefault { + target.MeshGateway.Mode = proxyDefault.MeshGateway.Mode } if c.overrideMeshGateway.Mode != structs.MeshGatewayModeDefault { @@ -987,7 +989,7 @@ RESOLVE_AGAIN: target, failover.Service, failover.ServiceSubset, - target.Partition, + failover.Partition, failover.Namespace, dc, ) @@ -1001,7 +1003,7 @@ RESOLVE_AGAIN: target, failover.Service, failover.ServiceSubset, - target.Partition, + failover.Partition, failover.Namespace, "", ) diff --git a/agent/consul/discoverychain/compile_test.go b/agent/consul/discoverychain/compile_test.go index 360b40e48e..1cd5758154 100644 --- a/agent/consul/discoverychain/compile_test.go +++ b/agent/consul/discoverychain/compile_test.go @@ -158,14 +158,14 @@ func testcase_JustRouterWithDefaults() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "router:main.default", + StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "router:main.default": { + "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, - Name: "main.default", + Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { - Definition: newDefaultServiceRoute("main", "default"), + Definition: newDefaultServiceRoute("main", "default", "default"), NextNode: "resolver:main.default.default.dc1", }, }, @@ -210,11 +210,11 @@ func testcase_JustRouterWithNoDestination() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "router:main.default", + StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "router:main.default": { + "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, - Name: "main.default", + Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { Definition: &structs.ServiceRoute{ @@ -227,7 +227,7 @@ func testcase_JustRouterWithNoDestination() compileTestCase { NextNode: "resolver:main.default.default.dc1", }, { - Definition: newDefaultServiceRoute("main", "default"), + Definition: newDefaultServiceRoute("main", "default", "default"), NextNode: "resolver:main.default.default.dc1", }, }, @@ -270,14 +270,14 @@ func testcase_RouterWithDefaults_NoSplit_WithResolver() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "router:main.default", + StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "router:main.default": { + "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, - Name: "main.default", + Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { - Definition: newDefaultServiceRoute("main", "default"), + Definition: newDefaultServiceRoute("main", "default", "default"), NextNode: "resolver:main.default.default.dc1", }, }, @@ -321,21 +321,21 @@ func testcase_RouterWithDefaults_WithNoopSplit_DefaultResolver() compileTestCase expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "router:main.default", + StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "router:main.default": { + "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, - Name: "main.default", + Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { - Definition: newDefaultServiceRoute("main", "default"), - NextNode: "splitter:main.default", + Definition: newDefaultServiceRoute("main", "default", "default"), + NextNode: "splitter:main.default.default", }, }, }, - "splitter:main.default": { + "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main.default", + Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ @@ -386,21 +386,21 @@ func testcase_NoopSplit_DefaultResolver_ProtocolFromProxyDefaults() compileTestC expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "router:main.default", + StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "router:main.default": { + "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, - Name: "main.default", + Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { - Definition: newDefaultServiceRoute("main", "default"), - NextNode: "splitter:main.default", + Definition: newDefaultServiceRoute("main", "default", "default"), + NextNode: "splitter:main.default.default", }, }, }, - "splitter:main.default": { + "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main.default", + Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ @@ -458,21 +458,21 @@ func testcase_RouterWithDefaults_WithNoopSplit_WithResolver() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "router:main.default", + StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "router:main.default": { + "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, - Name: "main.default", + Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { - Definition: newDefaultServiceRoute("main", "default"), - NextNode: "splitter:main.default", + Definition: newDefaultServiceRoute("main", "default", "default"), + NextNode: "splitter:main.default.default", }, }, }, - "splitter:main.default": { + "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main.default", + Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ @@ -542,18 +542,18 @@ func testcase_RouteBypassesSplit() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "router:main.default", + StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "router:main.default": { + "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, - Name: "main.default", + Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { Definition: &router.Routes[0], NextNode: "resolver:bypass.other.default.default.dc1", }, { - Definition: newDefaultServiceRoute("main", "default"), + Definition: newDefaultServiceRoute("main", "default", "default"), NextNode: "resolver:main.default.default.dc1", }, }, @@ -605,11 +605,11 @@ func testcase_NoopSplit_DefaultResolver() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "splitter:main.default", + StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "splitter:main.default": { + "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main.default", + Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ @@ -661,11 +661,11 @@ func testcase_NoopSplit_WithResolver() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "splitter:main.default", + StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "splitter:main.default": { + "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main.default", + Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ @@ -724,11 +724,11 @@ func testcase_SubsetSplit() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "splitter:main.default", + StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "splitter:main.default": { + "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main.default", + Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ @@ -801,11 +801,11 @@ func testcase_ServiceSplit() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "splitter:main.default", + StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "splitter:main.default": { + "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main.default", + Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ @@ -898,11 +898,11 @@ func testcase_SplitBypassesSplit() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "splitter:main.default", + StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "splitter:main.default": { + "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main.default", + Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ @@ -1053,13 +1053,14 @@ func testcase_DatacenterRedirect() compileTestCase { func testcase_DatacenterRedirect_WithMeshGateways() compileTestCase { entries := newEntries() - entries.GlobalProxy = &structs.ProxyConfigEntry{ + entries.AddProxyDefaults(&structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, MeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, }, - } + }) + entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", @@ -1300,13 +1301,15 @@ func testcase_DatacenterFailover() compileTestCase { func testcase_DatacenterFailover_WithMeshGateways() compileTestCase { entries := newEntries() - entries.GlobalProxy = &structs.ProxyConfigEntry{ + + entries.AddProxyDefaults(&structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, MeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, }, - } + }) + entries.AddResolvers( &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", @@ -1384,11 +1387,11 @@ func testcase_NoopSplit_WithDefaultSubset() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "splitter:main.default", + StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "splitter:main.default": { + "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main.default", + Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ @@ -1446,7 +1449,8 @@ func testcase_DefaultResolver() compileTestCase { func testcase_DefaultResolver_WithProxyDefaults() compileTestCase { entries := newEntries() - entries.GlobalProxy = &structs.ProxyConfigEntry{ + + entries.AddProxyDefaults(&structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ @@ -1455,7 +1459,7 @@ func testcase_DefaultResolver_WithProxyDefaults() compileTestCase { MeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, }, - } + }) expect := &structs.CompiledDiscoveryChain{ Protocol: "grpc", @@ -1699,11 +1703,11 @@ func testcase_MultiDatacenterCanary() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "splitter:main.default", + StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "splitter:main.default": { + "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main.default", + Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ @@ -1880,11 +1884,11 @@ func testcase_AllBellsAndWhistles() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "router:main.default", + StartNode: "router:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "router:main.default": { + "router:main.default.default": { Type: structs.DiscoveryGraphNodeTypeRouter, - Name: "main.default", + Name: "main.default.default", Routes: []*structs.DiscoveryRoute{ { Definition: &router.Routes[0], @@ -1892,17 +1896,17 @@ func testcase_AllBellsAndWhistles() compileTestCase { }, { Definition: &router.Routes[1], - NextNode: "splitter:svc-split.default", + NextNode: "splitter:svc-split.default.default", }, { - Definition: newDefaultServiceRoute("main", "default"), + Definition: newDefaultServiceRoute("main", "default", "default"), NextNode: "resolver:default-subset.main.default.default.dc1", }, }, }, - "splitter:svc-split.default": { + "splitter:svc-split.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "svc-split.default", + Name: "svc-split.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ @@ -2455,11 +2459,11 @@ func testcase_LBSplitterAndResolver() compileTestCase { expect := &structs.CompiledDiscoveryChain{ Protocol: "http", - StartNode: "splitter:main.default", + StartNode: "splitter:main.default.default", Nodes: map[string]*structs.DiscoveryGraphNode{ - "splitter:main.default": { + "splitter:main.default.default": { Type: structs.DiscoveryGraphNodeTypeSplitter, - Name: "main.default", + Name: "main.default.default", Splits: []*structs.DiscoverySplit{ { Definition: &structs.ServiceSplit{ @@ -2642,13 +2646,13 @@ func newSimpleRoute(name string, muts ...func(*structs.ServiceRoute)) structs.Se } func setGlobalProxyProtocol(entries *structs.DiscoveryChainConfigEntries, protocol string) { - entries.GlobalProxy = &structs.ProxyConfigEntry{ + entries.AddProxyDefaults(&structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": protocol, }, - } + }) } func setServiceProtocol(entries *structs.DiscoveryChainConfigEntries, name, protocol string) { diff --git a/agent/consul/state/config_entry.go b/agent/consul/state/config_entry.go index 594e49e1a5..434e776d4d 100644 --- a/agent/consul/state/config_entry.go +++ b/agent/consul/state/config_entry.go @@ -880,24 +880,21 @@ func readDiscoveryChainConfigEntriesTxn( sid := structs.NewServiceID(serviceName, entMeta) - // Grab the proxy defaults if they exist. - idx, proxy, err := getProxyConfigEntryTxn(tx, ws, structs.ProxyConfigGlobal, overrides, entMeta) - if err != nil { - return 0, nil, err - } else if proxy != nil { - res.GlobalProxy = proxy - } - - // At every step we'll need service defaults. + // At every step we'll need service and proxy defaults. todoDefaults[sid] = struct{}{} + var maxIdx uint64 + // first fetch the router, of which we only collect 1 per chain eval - _, router, err := getRouterConfigEntryTxn(tx, ws, serviceName, overrides, entMeta) + idx, router, err := getRouterConfigEntryTxn(tx, ws, serviceName, overrides, entMeta) if err != nil { return 0, nil, err } else if router != nil { res.Routers[sid] = router } + if idx > maxIdx { + maxIdx = idx + } if router != nil { for _, svc := range router.ListRelatedServices() { @@ -922,10 +919,13 @@ func readDiscoveryChainConfigEntriesTxn( // Yes, even for splitters. todoDefaults[splitID] = struct{}{} - _, splitter, err := getSplitterConfigEntryTxn(tx, ws, splitID.ID, overrides, &splitID.EnterpriseMeta) + idx, splitter, err := getSplitterConfigEntryTxn(tx, ws, splitID.ID, overrides, &splitID.EnterpriseMeta) if err != nil { return 0, nil, err } + if idx > maxIdx { + maxIdx = idx + } if splitter == nil { res.Splitters[splitID] = nil @@ -959,10 +959,13 @@ func readDiscoveryChainConfigEntriesTxn( // And resolvers, too. todoDefaults[resolverID] = struct{}{} - _, resolver, err := getResolverConfigEntryTxn(tx, ws, resolverID.ID, overrides, &resolverID.EnterpriseMeta) + idx, resolver, err := getResolverConfigEntryTxn(tx, ws, resolverID.ID, overrides, &resolverID.EnterpriseMeta) if err != nil { return 0, nil, err } + if idx > maxIdx { + maxIdx = idx + } if resolver == nil { res.Resolvers[resolverID] = nil @@ -987,16 +990,31 @@ func readDiscoveryChainConfigEntriesTxn( continue // already fetched } - _, entry, err := getServiceConfigEntryTxn(tx, ws, svcID.ID, overrides, &svcID.EnterpriseMeta) + if _, ok := res.ProxyDefaults[svcID.PartitionOrDefault()]; !ok { + idx, proxy, err := getProxyConfigEntryTxn(tx, ws, structs.ProxyConfigGlobal, overrides, &svcID.EnterpriseMeta) + if err != nil { + return 0, nil, err + } + if idx > maxIdx { + maxIdx = idx + } + if proxy != nil { + res.ProxyDefaults[proxy.PartitionOrDefault()] = proxy + } + } + + idx, entry, err := getServiceConfigEntryTxn(tx, ws, svcID.ID, overrides, &svcID.EnterpriseMeta) if err != nil { return 0, nil, err } + if idx > maxIdx { + maxIdx = idx + } if entry == nil { res.Services[svcID] = nil continue } - res.Services[svcID] = entry } diff --git a/agent/consul/state/config_entry_test.go b/agent/consul/state/config_entry_test.go index b47d9a3565..daddc9bb83 100644 --- a/agent/consul/state/config_entry_test.go +++ b/agent/consul/state/config_entry_test.go @@ -1347,6 +1347,13 @@ func entrySetToKindNames(entrySet *structs.DiscoveryChainConfigEntries) []Config &entry.EnterpriseMeta, )) } + for _, entry := range entrySet.ProxyDefaults { + out = append(out, NewConfigEntryKindName( + entry.Kind, + entry.Name, + &entry.EnterpriseMeta, + )) + } return out } diff --git a/agent/structs/config_entry_discoverychain.go b/agent/structs/config_entry_discoverychain.go index 35287f442a..37b2040577 100644 --- a/agent/structs/config_entry_discoverychain.go +++ b/agent/structs/config_entry_discoverychain.go @@ -119,6 +119,9 @@ func (e *ServiceRouterConfigEntry) Normalize() error { if route.Destination != nil && route.Destination.Namespace == "" { route.Destination.Namespace = e.EnterpriseMeta.NamespaceOrEmpty() } + if route.Destination != nil && route.Destination.Partition == "" { + route.Destination.Partition = e.EnterpriseMeta.PartitionOrEmpty() + } } return nil @@ -381,6 +384,13 @@ type ServiceRouteDestination struct { // splitting. Namespace string `json:",omitempty"` + // Partition is the partition to resolve the service from instead of the + // current partition. If empty the current partition is assumed. + // + // If this field is specified then this route is ineligible for further + // splitting. + Partition string `json:",omitempty"` + // PrefixRewrite allows for the proxied request to have its matching path // prefix modified before being sent to the destination. Described more // below in the envoy implementation section. @@ -557,8 +567,8 @@ func (e *ServiceSplitterConfigEntry) Validate() error { } if _, ok := found[splitKey]; ok { return fmt.Errorf( - "split destination occurs more than once: service=%q, subset=%q, namespace=%q", - splitKey.Service, splitKey.ServiceSubset, splitKey.Namespace, + "split destination occurs more than once: service=%q, subset=%q, namespace=%q, partition=%q", + splitKey.Service, splitKey.ServiceSubset, splitKey.Namespace, splitKey.Partition, ) } found[splitKey] = struct{}{} @@ -665,7 +675,12 @@ type ServiceSplit struct { // splitting. Namespace string `json:",omitempty"` - // NOTE: Partition is not represented here by design. Do not add it. + // Partition is the partition to resolve the service from instead of the + // current partition. If empty the current partition is assumed (optional). + // + // If this field is specified then this route is ineligible for further + // splitting. + Partition string `json:",omitempty"` // NOTE: Any configuration added to Splits that needs to be passed to the // proxy needs special handling MergeParent below. @@ -930,9 +945,13 @@ func (e *ServiceResolverConfigEntry) Validate() error { } if e.Redirect != nil { - if e.PartitionOrEmpty() != acl.DefaultPartitionName && e.Redirect.Datacenter != "" { - return fmt.Errorf("Cross datacenters redirect is not allowed for non default partition") + if !e.InDefaultPartition() && e.Redirect.Datacenter != "" { + return fmt.Errorf("Cross-datacenter redirect is not supported in non-default partitions") } + if PartitionOrDefault(e.Redirect.Partition) != e.PartitionOrDefault() && e.Redirect.Datacenter != "" { + return fmt.Errorf("Cross-datacenter and cross-partition redirect is not supported") + } + r := e.Redirect if len(e.Failover) > 0 { @@ -941,7 +960,7 @@ func (e *ServiceResolverConfigEntry) Validate() error { // TODO(rb): prevent subsets and default subsets from being defined? - if r.Service == "" && r.ServiceSubset == "" && r.Namespace == "" && r.Datacenter == "" { + if r.Service == "" && r.ServiceSubset == "" && r.Namespace == "" && r.Partition == "" && r.Datacenter == "" { return fmt.Errorf("Redirect is empty") } @@ -952,6 +971,9 @@ func (e *ServiceResolverConfigEntry) Validate() error { if r.Namespace != "" { return fmt.Errorf("Redirect.Namespace defined without Redirect.Service") } + if r.Partition != "" { + return fmt.Errorf("Redirect.Partition defined without Redirect.Service") + } } else if r.Service == e.Name { if r.ServiceSubset != "" && !isSubset(r.ServiceSubset) { return fmt.Errorf("Redirect.ServiceSubset %q is not a valid subset of %q", r.ServiceSubset, r.Service) @@ -962,15 +984,19 @@ func (e *ServiceResolverConfigEntry) Validate() error { if len(e.Failover) > 0 { for subset, f := range e.Failover { - if e.PartitionOrEmpty() != acl.DefaultPartitionName && len(f.Datacenters) != 0 { - return fmt.Errorf("Cross datacenters failover is not allowed for non default partition") + if !e.InDefaultPartition() && len(f.Datacenters) != 0 { + return fmt.Errorf("Cross-datacenter failover is not supported in non-default partitions") } + if PartitionOrDefault(f.Partition) != e.PartitionOrDefault() && len(f.Datacenters) != 0 { + return fmt.Errorf("Cross-datacenter and cross-partition failover is not supported") + } + if subset != "*" && !isSubset(subset) { return fmt.Errorf("Bad Failover[%q]: not a valid subset", subset) } - if f.Service == "" && f.ServiceSubset == "" && f.Namespace == "" && len(f.Datacenters) == 0 { - return fmt.Errorf("Bad Failover[%q] one of Service, ServiceSubset, Namespace, or Datacenters is required", subset) + if f.Service == "" && f.ServiceSubset == "" && f.Namespace == "" && f.Partition == "" && len(f.Datacenters) == 0 { + return fmt.Errorf("Bad Failover[%q] one of Service, ServiceSubset, Namespace, Partition, or Datacenters is required", subset) } if f.ServiceSubset != "" { @@ -1141,6 +1167,10 @@ type ServiceResolverRedirect struct { // current one (optional). Namespace string `json:",omitempty"` + // Partition is the partition to resolve the service from instead of the + // current one (optional). + Partition string `json:",omitempty"` + // Datacenter is the datacenter to resolve the service from instead of the // current one (optional). Datacenter string `json:",omitempty"` @@ -1172,6 +1202,13 @@ type ServiceResolverFailover struct { // This is a DESTINATION during failover. Namespace string `json:",omitempty"` + // Partition is the partition to resolve the requested service from to form + // the failover group of instances. If empty the current partition is used + // (optional). + // + // This is a DESTINATION during failover. + Partition string `json:",omitempty"` + // Datacenters is a fixed list of datacenters to try. We never try a // datacenter multiple times, so those are subtracted from this list before // proceeding. @@ -1309,19 +1346,20 @@ func canWriteDiscoveryChain(entry discoveryChainConfigEntry, authz acl.Authorize // DiscoveryChainConfigEntries wraps just the raw cross-referenced config // entries. None of these are defaulted. type DiscoveryChainConfigEntries struct { - Routers map[ServiceID]*ServiceRouterConfigEntry - Splitters map[ServiceID]*ServiceSplitterConfigEntry - Resolvers map[ServiceID]*ServiceResolverConfigEntry - Services map[ServiceID]*ServiceConfigEntry - GlobalProxy *ProxyConfigEntry + Routers map[ServiceID]*ServiceRouterConfigEntry + Splitters map[ServiceID]*ServiceSplitterConfigEntry + Resolvers map[ServiceID]*ServiceResolverConfigEntry + Services map[ServiceID]*ServiceConfigEntry + ProxyDefaults map[string]*ProxyConfigEntry } func NewDiscoveryChainConfigEntries() *DiscoveryChainConfigEntries { return &DiscoveryChainConfigEntries{ - Routers: make(map[ServiceID]*ServiceRouterConfigEntry), - Splitters: make(map[ServiceID]*ServiceSplitterConfigEntry), - Resolvers: make(map[ServiceID]*ServiceResolverConfigEntry), - Services: make(map[ServiceID]*ServiceConfigEntry), + Routers: make(map[ServiceID]*ServiceRouterConfigEntry), + Splitters: make(map[ServiceID]*ServiceSplitterConfigEntry), + Resolvers: make(map[ServiceID]*ServiceResolverConfigEntry), + Services: make(map[ServiceID]*ServiceConfigEntry), + ProxyDefaults: make(map[string]*ProxyConfigEntry), } } @@ -1353,6 +1391,13 @@ func (e *DiscoveryChainConfigEntries) GetService(sid ServiceID) *ServiceConfigEn return nil } +func (e *DiscoveryChainConfigEntries) GetProxyDefaults(partition string) *ProxyConfigEntry { + if e.ProxyDefaults != nil { + return e.ProxyDefaults[partition] + } + return nil +} + // AddRouters adds router configs. Convenience function for testing. func (e *DiscoveryChainConfigEntries) AddRouters(entries ...*ServiceRouterConfigEntry) { if e.Routers == nil { @@ -1393,6 +1438,16 @@ func (e *DiscoveryChainConfigEntries) AddServices(entries ...*ServiceConfigEntry } } +// AddProxyDefaults adds proxy-defaults configs. Convenience function for testing. +func (e *DiscoveryChainConfigEntries) AddProxyDefaults(entries ...*ProxyConfigEntry) { + if e.ProxyDefaults == nil { + e.ProxyDefaults = make(map[string]*ProxyConfigEntry) + } + for _, entry := range entries { + e.ProxyDefaults[entry.PartitionOrDefault()] = entry + } +} + // AddEntries adds generic configs. Convenience function for testing. Panics on // operator error. func (e *DiscoveryChainConfigEntries) AddEntries(entries ...ConfigEntry) { @@ -1410,7 +1465,7 @@ func (e *DiscoveryChainConfigEntries) AddEntries(entries ...ConfigEntry) { if entry.GetName() != ProxyConfigGlobal { panic("the only supported proxy-defaults name is '" + ProxyConfigGlobal + "'") } - e.GlobalProxy = entry.(*ProxyConfigEntry) + e.AddProxyDefaults(entry.(*ProxyConfigEntry)) default: panic("unhandled config entry kind: " + entry.GetKind()) } @@ -1418,7 +1473,7 @@ func (e *DiscoveryChainConfigEntries) AddEntries(entries ...ConfigEntry) { } func (e *DiscoveryChainConfigEntries) IsEmpty() bool { - return e.IsChainEmpty() && len(e.Services) == 0 && e.GlobalProxy == nil + return e.IsChainEmpty() && len(e.Services) == 0 && len(e.ProxyDefaults) == 0 } func (e *DiscoveryChainConfigEntries) IsChainEmpty() bool { diff --git a/agent/structs/config_entry_discoverychain_test.go b/agent/structs/config_entry_discoverychain_test.go index a3fb49b4ae..e2554977ab 100644 --- a/agent/structs/config_entry_discoverychain_test.go +++ b/agent/structs/config_entry_discoverychain_test.go @@ -695,7 +695,7 @@ func TestServiceResolverConfigEntry(t *testing.T) { "v1": {}, }, }, - validateErr: `Bad Failover["v1"] one of Service, ServiceSubset, Namespace, or Datacenters is required`, + validateErr: `Bad Failover["v1"] one of Service, ServiceSubset, Namespace, Partition, or Datacenters is required`, }, { name: "failover to self using invalid subset", diff --git a/test/integration/connect/envoy/run-tests.sh b/test/integration/connect/envoy/run-tests.sh index ce507845a4..b5978ad294 100755 --- a/test/integration/connect/envoy/run-tests.sh +++ b/test/integration/connect/envoy/run-tests.sh @@ -513,6 +513,14 @@ function run_container_s2-secondary { common_run_container_service s2-secondary secondary 8181 8179 } +function run_container_s2-ap1 { + common_run_container_service s2 ap1 8480 8479 +} + +function run_container_s3-ap1 { + common_run_container_service s3 ap1 8580 8579 +} + function common_run_container_sidecar_proxy { local service="$1" local CLUSTER="$2" @@ -581,6 +589,14 @@ function run_container_s2-sidecar-proxy-secondary { common_run_container_sidecar_proxy s2 secondary } +function run_container_s2-ap1-sidecar-proxy { + common_run_container_sidecar_proxy s2 ap1 +} + +function run_container_s3-ap1-sidecar-proxy { + common_run_container_sidecar_proxy s3 ap1 +} + function common_run_container_gateway { local name="$1" local DC="$2"