Skip to content

Commit

Permalink
[API Gateway] Fix targeting service splitters in HTTPRoutes (#16350)
Browse files Browse the repository at this point in the history
* [API Gateway] Fix targeting service splitters in HTTPRoutes

* Fix test description
  • Loading branch information
Andrew Stucki authored Feb 22, 2023
1 parent 823fc82 commit 18e2ee7
Show file tree
Hide file tree
Showing 9 changed files with 419 additions and 4 deletions.
39 changes: 39 additions & 0 deletions agent/consul/discoverychain/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"hash/crc32"
"sort"
"strconv"
"strings"

"github.com/hashicorp/consul/agent/configentry"
"github.com/hashicorp/consul/agent/structs"
Expand Down Expand Up @@ -126,6 +127,23 @@ func (l *GatewayChainSynthesizer) Synthesize(chains ...*structs.CompiledDiscover
if err != nil {
return nil, nil, err
}

// fix up the nodes for the terminal targets to either be a splitter or resolver if there is no splitter present
for name, node := range compiled.Nodes {
switch node.Type {
// we should only have these two types
case structs.DiscoveryGraphNodeTypeRouter:
for i, route := range node.Routes {
node.Routes[i].NextNode = targetForResolverNode(route.NextNode, chains)
}
case structs.DiscoveryGraphNodeTypeSplitter:
for i, split := range node.Splits {
node.Splits[i].NextNode = targetForResolverNode(split.NextNode, chains)
}
}
compiled.Nodes[name] = node
}

for _, c := range chains {
for id, target := range c.Targets {
compiled.Targets[id] = target
Expand Down Expand Up @@ -177,6 +195,27 @@ func (l *GatewayChainSynthesizer) consolidateHTTPRoutes() []structs.HTTPRouteCon
return routes
}

func targetForResolverNode(nodeName string, chains []*structs.CompiledDiscoveryChain) string {
resolverPrefix := structs.DiscoveryGraphNodeTypeResolver + ":"
splitterPrefix := structs.DiscoveryGraphNodeTypeSplitter + ":"

if !strings.HasPrefix(nodeName, resolverPrefix) {
return nodeName
}

splitterName := splitterPrefix + strings.TrimPrefix(nodeName, resolverPrefix)

for _, c := range chains {
for name, node := range c.Nodes {
if node.IsSplitter() && strings.HasPrefix(splitterName, name) {
return name
}
}
}

return nodeName
}

func hostsKey(hosts ...string) string {
sort.Strings(hosts)
hostsHash := crc32.NewIEEE()
Expand Down
254 changes: 254 additions & 0 deletions agent/consul/discoverychain/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package discoverychain
import (
"testing"

"github.com/hashicorp/consul/agent/configentry"
"github.com/hashicorp/consul/agent/structs"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -640,3 +641,256 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) {
})
}
}

func TestGatewayChainSynthesizer_ComplexChain(t *testing.T) {
t.Parallel()

cases := map[string]struct {
synthesizer *GatewayChainSynthesizer
route *structs.HTTPRouteConfigEntry
entries []structs.ConfigEntry
expectedDiscoveryChain *structs.CompiledDiscoveryChain
}{
"HTTP-Route with nested splitters": {
synthesizer: NewGatewayChainSynthesizer("dc1", "domain", "suffix", &structs.APIGatewayConfigEntry{
Kind: structs.APIGateway,
Name: "gateway",
}),
route: &structs.HTTPRouteConfigEntry{
Kind: structs.HTTPRoute,
Name: "test",
Rules: []structs.HTTPRouteRule{{
Services: []structs.HTTPService{{
Name: "splitter-one",
}},
}},
},
entries: []structs.ConfigEntry{
&structs.ServiceSplitterConfigEntry{
Kind: structs.ServiceSplitter,
Name: "splitter-one",
Splits: []structs.ServiceSplit{{
Service: "service-one",
Weight: 50,
}, {
Service: "splitter-two",
Weight: 50,
}},
},
&structs.ServiceSplitterConfigEntry{
Kind: structs.ServiceSplitter,
Name: "splitter-two",
Splits: []structs.ServiceSplit{{
Service: "service-two",
Weight: 50,
}, {
Service: "service-three",
Weight: 50,
}},
},
&structs.ProxyConfigEntry{
Kind: structs.ProxyConfigGlobal,
Name: "global",
Config: map[string]interface{}{
"protocol": "http",
},
},
},
expectedDiscoveryChain: &structs.CompiledDiscoveryChain{
ServiceName: "gateway-suffix-9b9265b",
Namespace: "default",
Partition: "default",
Datacenter: "dc1",
Protocol: "http",
StartNode: "router:gateway-suffix-9b9265b.default.default",
Nodes: map[string]*structs.DiscoveryGraphNode{
"resolver:gateway-suffix-9b9265b.default.default.dc1": {
Type: "resolver",
Name: "gateway-suffix-9b9265b.default.default.dc1",
Resolver: &structs.DiscoveryResolver{
Target: "gateway-suffix-9b9265b.default.default.dc1",
Default: true,
ConnectTimeout: 5000000000,
},
},
"resolver:service-one.default.default.dc1": {
Type: "resolver",
Name: "service-one.default.default.dc1",
Resolver: &structs.DiscoveryResolver{
Target: "service-one.default.default.dc1",
Default: true,
ConnectTimeout: 5000000000,
},
},
"resolver:service-three.default.default.dc1": {
Type: "resolver",
Name: "service-three.default.default.dc1",
Resolver: &structs.DiscoveryResolver{
Target: "service-three.default.default.dc1",
Default: true,
ConnectTimeout: 5000000000,
},
},
"resolver:service-two.default.default.dc1": {
Type: "resolver",
Name: "service-two.default.default.dc1",
Resolver: &structs.DiscoveryResolver{
Target: "service-two.default.default.dc1",
Default: true,
ConnectTimeout: 5000000000,
},
},
"resolver:splitter-one.default.default.dc1": {
Type: "resolver",
Name: "splitter-one.default.default.dc1",
Resolver: &structs.DiscoveryResolver{
Target: "splitter-one.default.default.dc1",
Default: true,
ConnectTimeout: 5000000000,
},
},
"router:gateway-suffix-9b9265b.default.default": {
Type: "router",
Name: "gateway-suffix-9b9265b.default.default",
Routes: []*structs.DiscoveryRoute{{
Definition: &structs.ServiceRoute{
Match: &structs.ServiceRouteMatch{
HTTP: &structs.ServiceRouteHTTPMatch{
PathPrefix: "/",
},
},
Destination: &structs.ServiceRouteDestination{
Service: "splitter-one",
Partition: "default",
Namespace: "default",
RequestHeaders: &structs.HTTPHeaderModifiers{
Add: make(map[string]string),
Set: make(map[string]string),
},
},
},
NextNode: "splitter:splitter-one.default.default",
}, {
Definition: &structs.ServiceRoute{
Match: &structs.ServiceRouteMatch{
HTTP: &structs.ServiceRouteHTTPMatch{
PathPrefix: "/",
},
},
Destination: &structs.ServiceRouteDestination{
Service: "gateway-suffix-9b9265b",
Partition: "default",
Namespace: "default",
},
},
NextNode: "resolver:gateway-suffix-9b9265b.default.default.dc1",
}},
},
"splitter:splitter-one.default.default": {
Type: structs.DiscoveryGraphNodeTypeSplitter,
Name: "splitter-one.default.default",
Splits: []*structs.DiscoverySplit{{
Definition: &structs.ServiceSplit{
Weight: 50,
Service: "service-one",
},
Weight: 50,
NextNode: "resolver:service-one.default.default.dc1",
}, {
Definition: &structs.ServiceSplit{
Weight: 50,
Service: "service-two",
},
Weight: 25,
NextNode: "resolver:service-two.default.default.dc1",
}, {
Definition: &structs.ServiceSplit{
Weight: 50,
Service: "service-three",
},
Weight: 25,
NextNode: "resolver:service-three.default.default.dc1",
}},
},
}, Targets: map[string]*structs.DiscoveryTarget{
"gateway-suffix-9b9265b.default.default.dc1": {
ID: "gateway-suffix-9b9265b.default.default.dc1",
Service: "gateway-suffix-9b9265b",
Datacenter: "dc1",
Partition: "default",
Namespace: "default",
ConnectTimeout: 5000000000,
SNI: "gateway-suffix-9b9265b.default.dc1.internal.domain",
Name: "gateway-suffix-9b9265b.default.dc1.internal.domain",
},
"service-one.default.default.dc1": {
ID: "service-one.default.default.dc1",
Service: "service-one",
Datacenter: "dc1",
Partition: "default",
Namespace: "default",
ConnectTimeout: 5000000000,
SNI: "service-one.default.dc1.internal.domain",
Name: "service-one.default.dc1.internal.domain",
},
"service-three.default.default.dc1": {
ID: "service-three.default.default.dc1",
Service: "service-three",
Datacenter: "dc1",
Partition: "default",
Namespace: "default",
ConnectTimeout: 5000000000,
SNI: "service-three.default.dc1.internal.domain",
Name: "service-three.default.dc1.internal.domain",
},
"service-two.default.default.dc1": {
ID: "service-two.default.default.dc1",
Service: "service-two",
Datacenter: "dc1",
Partition: "default",
Namespace: "default",
ConnectTimeout: 5000000000,
SNI: "service-two.default.dc1.internal.domain",
Name: "service-two.default.dc1.internal.domain",
},
"splitter-one.default.default.dc1": {
ID: "splitter-one.default.default.dc1",
Service: "splitter-one",
Datacenter: "dc1",
Partition: "default",
Namespace: "default",
ConnectTimeout: 5000000000,
SNI: "splitter-one.default.dc1.internal.domain",
Name: "splitter-one.default.dc1.internal.domain",
},
}},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
service := tc.entries[0]
entries := configentry.NewDiscoveryChainSet()
entries.AddEntries(tc.entries...)
compiled, err := Compile(CompileRequest{
ServiceName: service.GetName(),
EvaluateInNamespace: service.GetEnterpriseMeta().NamespaceOrDefault(),
EvaluateInPartition: service.GetEnterpriseMeta().PartitionOrDefault(),
EvaluateInDatacenter: "dc1",
EvaluateInTrustDomain: "domain",
Entries: entries,
})
require.NoError(t, err)

tc.synthesizer.SetHostname("*")
tc.synthesizer.AddHTTPRoute(*tc.route)

chains := []*structs.CompiledDiscoveryChain{compiled}
_, discoveryChains, err := tc.synthesizer.Synthesize(chains...)

require.NoError(t, err)
require.Len(t, discoveryChains, 1)
require.Equal(t, tc.expectedDiscoveryChain, discoveryChains[0])
})
}
}
8 changes: 4 additions & 4 deletions agent/structs/config_entry_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ func (e *HTTPRouteConfigEntry) Normalize() error {
for i, parent := range e.Parents {
if parent.Kind == "" {
parent.Kind = APIGateway
parent.EnterpriseMeta.Normalize()
e.Parents[i] = parent
}
parent.EnterpriseMeta.Normalize()
e.Parents[i] = parent
}

for i, rule := range e.Rules {
Expand Down Expand Up @@ -505,9 +505,9 @@ func (e *TCPRouteConfigEntry) Normalize() error {
for i, parent := range e.Parents {
if parent.Kind == "" {
parent.Kind = APIGateway
parent.EnterpriseMeta.Normalize()
e.Parents[i] = parent
}
parent.EnterpriseMeta.Normalize()
e.Parents[i] = parent
}

for i, service := range e.Services {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

snapshot_envoy_admin localhost:20000 api-gateway primary || true
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
services {
name = "api-gateway"
kind = "api-gateway"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
services {
id = "s3"
name = "s3"
port = 8182

connect {
sidecar_service {}
}
}
Loading

0 comments on commit 18e2ee7

Please sign in to comment.