Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport of [API Gateway] Fix targeting service splitters in HTTPRoutes into release/1.15.x #16353

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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