mirror of
https://github.com/status-im/consul.git
synced 2025-02-21 09:58:26 +00:00
[API Gateway] Fix targeting service splitters in HTTPRoutes (#16350)
* [API Gateway] Fix targeting service splitters in HTTPRoutes * Fix test description
This commit is contained in:
parent
823fc821fa
commit
18e2ee77ca
@ -5,6 +5,7 @@ import (
|
|||||||
"hash/crc32"
|
"hash/crc32"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/configentry"
|
"github.com/hashicorp/consul/agent/configentry"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
@ -126,6 +127,23 @@ func (l *GatewayChainSynthesizer) Synthesize(chains ...*structs.CompiledDiscover
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
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 _, c := range chains {
|
||||||
for id, target := range c.Targets {
|
for id, target := range c.Targets {
|
||||||
compiled.Targets[id] = target
|
compiled.Targets[id] = target
|
||||||
@ -177,6 +195,27 @@ func (l *GatewayChainSynthesizer) consolidateHTTPRoutes() []structs.HTTPRouteCon
|
|||||||
return routes
|
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 {
|
func hostsKey(hosts ...string) string {
|
||||||
sort.Strings(hosts)
|
sort.Strings(hosts)
|
||||||
hostsHash := crc32.NewIEEE()
|
hostsHash := crc32.NewIEEE()
|
||||||
|
@ -3,6 +3,7 @@ package discoverychain
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/configentry"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -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])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -79,10 +79,10 @@ func (e *HTTPRouteConfigEntry) Normalize() error {
|
|||||||
for i, parent := range e.Parents {
|
for i, parent := range e.Parents {
|
||||||
if parent.Kind == "" {
|
if parent.Kind == "" {
|
||||||
parent.Kind = APIGateway
|
parent.Kind = APIGateway
|
||||||
|
}
|
||||||
parent.EnterpriseMeta.Normalize()
|
parent.EnterpriseMeta.Normalize()
|
||||||
e.Parents[i] = parent
|
e.Parents[i] = parent
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for i, rule := range e.Rules {
|
for i, rule := range e.Rules {
|
||||||
for j, match := range rule.Matches {
|
for j, match := range rule.Matches {
|
||||||
@ -505,10 +505,10 @@ func (e *TCPRouteConfigEntry) Normalize() error {
|
|||||||
for i, parent := range e.Parents {
|
for i, parent := range e.Parents {
|
||||||
if parent.Kind == "" {
|
if parent.Kind == "" {
|
||||||
parent.Kind = APIGateway
|
parent.Kind = APIGateway
|
||||||
|
}
|
||||||
parent.EnterpriseMeta.Normalize()
|
parent.EnterpriseMeta.Normalize()
|
||||||
e.Parents[i] = parent
|
e.Parents[i] = parent
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for i, service := range e.Services {
|
for i, service := range e.Services {
|
||||||
service.EnterpriseMeta.Normalize()
|
service.EnterpriseMeta.Normalize()
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
snapshot_envoy_admin localhost:20000 api-gateway primary || true
|
@ -0,0 +1,4 @@
|
|||||||
|
services {
|
||||||
|
name = "api-gateway"
|
||||||
|
kind = "api-gateway"
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
services {
|
||||||
|
id = "s3"
|
||||||
|
name = "s3"
|
||||||
|
port = 8182
|
||||||
|
|
||||||
|
connect {
|
||||||
|
sidecar_service {}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
upsert_config_entry primary '
|
||||||
|
kind = "api-gateway"
|
||||||
|
name = "api-gateway"
|
||||||
|
listeners = [
|
||||||
|
{
|
||||||
|
name = "listener-one"
|
||||||
|
port = 9999
|
||||||
|
protocol = "http"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
'
|
||||||
|
|
||||||
|
upsert_config_entry primary '
|
||||||
|
Kind = "proxy-defaults"
|
||||||
|
Name = "global"
|
||||||
|
Config {
|
||||||
|
protocol = "http"
|
||||||
|
}
|
||||||
|
'
|
||||||
|
|
||||||
|
upsert_config_entry primary '
|
||||||
|
kind = "http-route"
|
||||||
|
name = "api-gateway-route-one"
|
||||||
|
rules = [
|
||||||
|
{
|
||||||
|
services = [
|
||||||
|
{
|
||||||
|
name = "splitter-one"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
parents = [
|
||||||
|
{
|
||||||
|
name = "api-gateway"
|
||||||
|
sectionName = "listener-one"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
'
|
||||||
|
|
||||||
|
upsert_config_entry primary '
|
||||||
|
kind = "service-splitter"
|
||||||
|
name = "splitter-one"
|
||||||
|
splits = [
|
||||||
|
{
|
||||||
|
weight = 50,
|
||||||
|
service = "s1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
weight = 50,
|
||||||
|
service = "splitter-two"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
'
|
||||||
|
|
||||||
|
upsert_config_entry primary '
|
||||||
|
kind = "service-splitter"
|
||||||
|
name = "splitter-two"
|
||||||
|
splits = [
|
||||||
|
{
|
||||||
|
weight = 50,
|
||||||
|
service = "s2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
weight = 50,
|
||||||
|
service = "s3"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
'
|
||||||
|
|
||||||
|
register_services primary
|
||||||
|
|
||||||
|
gen_envoy_bootstrap api-gateway 20000 primary true
|
||||||
|
gen_envoy_bootstrap s1 19000
|
||||||
|
gen_envoy_bootstrap s2 19001
|
||||||
|
gen_envoy_bootstrap s3 19002
|
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export REQUIRED_SERVICES="$DEFAULT_REQUIRED_SERVICES api-gateway-primary"
|
@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
load helpers
|
||||||
|
|
||||||
|
@test "api gateway proxy admin is up on :20000" {
|
||||||
|
retry_default curl -f -s localhost:20000/stats -o /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "api gateway should be accepted and not conflicted" {
|
||||||
|
assert_config_entry_status Accepted True Accepted primary api-gateway api-gateway
|
||||||
|
assert_config_entry_status Conflicted False NoConflict primary api-gateway api-gateway
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "api gateway should have healthy endpoints for s1" {
|
||||||
|
assert_config_entry_status Bound True Bound primary http-route api-gateway-route-one
|
||||||
|
assert_upstream_has_endpoints_in_status 127.0.0.1:20000 s1 HEALTHY 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "api gateway should be able to connect to s1, s2, and s3 via configured port" {
|
||||||
|
run retry_default assert_expected_fortio_name_pattern ^FORTIO_NAME=s1$
|
||||||
|
run retry_default assert_expected_fortio_name_pattern ^FORTIO_NAME=s2$
|
||||||
|
run retry_default assert_expected_fortio_name_pattern ^FORTIO_NAME=s3$
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user