Skip to content

Commit 9138a97

Browse files
authored
Fix bug in service-resolver redirects if the destination uses a default resolver. (#6122)
Also: - add back an internal http endpoint to dump a compiled discovery chain for debugging purposes Before the CompiledDiscoveryChain.IsDefault() method would test: - is this chain just one resolver step? - is that resolver step just the default? But what I forgot to test: - is that resolver step for the same service that the chain represents? This last point is important because if you configured just one config entry: kind = "service-resolver" name = "web" redirect { service = "other" } and requested the chain for "web" you'd get back a **default** resolver for "other". In the xDS code the IsDefault() method is used to determine if this chain is "empty". If it is then we use the pre-discovery-chain logic that just uses data embedded in the Upstream object (and still lets the escape hatches function). In the example above that means certain parts of the xDS code were going to try referencing a cluster named "web..." despite the other parts of the xDS code maintaining clusters named "other...".
1 parent 67a36e3 commit 9138a97

File tree

10 files changed

+244
-9
lines changed

10 files changed

+244
-9
lines changed

agent/consul/discoverychain/compile_test.go

+53-8
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@ func TestCompile_NoEntries_NoInferDefaults(t *testing.T) {
2323
}
2424

2525
type compileTestCase struct {
26-
entries *structs.DiscoveryChainConfigEntries
27-
expect *structs.CompiledDiscoveryChain // the GroupResolverNodes map should have nil values
28-
expectErr string
29-
expectGraphErr bool
26+
entries *structs.DiscoveryChainConfigEntries
27+
// expect: the GroupResolverNodes map should have nil values
28+
expect *structs.CompiledDiscoveryChain
29+
// expectIsDefault tests behavior of CompiledDiscoveryChain.IsDefault()
30+
expectIsDefault bool
31+
expectErr string
32+
expectGraphErr bool
3033
}
3134

3235
func TestCompile(t *testing.T) {
@@ -40,7 +43,7 @@ func TestCompile(t *testing.T) {
4043
"router with defaults and noop split and resolver": testcase_RouterWithDefaults_WithNoopSplit_WithResolver(),
4144
"route bypasses splitter": testcase_RouteBypassesSplit(),
4245
"noop split": testcase_NoopSplit_DefaultResolver(),
43-
"noop split with protocol from proxy defaults": testcase_NoopSplit_DefaultResolver_ProcotolFromProxyDefaults(),
46+
"noop split with protocol from proxy defaults": testcase_NoopSplit_DefaultResolver_ProtocolFromProxyDefaults(),
4447
"noop split with resolver": testcase_NoopSplit_WithResolver(),
4548
"subset split": testcase_SubsetSplit(),
4649
"service split": testcase_ServiceSplit(),
@@ -55,6 +58,7 @@ func TestCompile(t *testing.T) {
5558
"resolver with default subset": testcase_Resolve_WithDefaultSubset(),
5659
"resolver with no entries and inferring defaults": testcase_DefaultResolver(),
5760
"default resolver with proxy defaults": testcase_DefaultResolver_WithProxyDefaults(),
61+
"service redirect to service with default resolver is not a default chain": testcase_RedirectToDefaultResolverIsNotDefaultChain(),
5862

5963
// TODO(rb): handle this case better: "circular split": testcase_CircularSplit(),
6064
"all the bells and whistles": testcase_AllBellsAndWhistles(),
@@ -125,6 +129,7 @@ func TestCompile(t *testing.T) {
125129
}
126130

127131
require.Equal(t, tc.expect, res)
132+
require.Equal(t, tc.expectIsDefault, res.IsDefault())
128133
}
129134
})
130135
}
@@ -298,7 +303,7 @@ func testcase_RouterWithDefaults_WithNoopSplit_DefaultResolver() compileTestCase
298303
return compileTestCase{entries: entries, expect: expect}
299304
}
300305

301-
func testcase_NoopSplit_DefaultResolver_ProcotolFromProxyDefaults() compileTestCase {
306+
func testcase_NoopSplit_DefaultResolver_ProtocolFromProxyDefaults() compileTestCase {
302307
entries := newEntries()
303308
setGlobalProxyProtocol(entries, "http")
304309

@@ -1230,7 +1235,7 @@ func testcase_DefaultResolver() compileTestCase {
12301235
newTarget("main", "", "default", "dc1"): nil,
12311236
},
12321237
}
1233-
return compileTestCase{entries: entries, expect: expect}
1238+
return compileTestCase{entries: entries, expect: expect, expectIsDefault: true}
12341239
}
12351240

12361241
func testcase_DefaultResolver_WithProxyDefaults() compileTestCase {
@@ -1273,7 +1278,47 @@ func testcase_DefaultResolver_WithProxyDefaults() compileTestCase {
12731278
newTarget("main", "", "default", "dc1"): nil,
12741279
},
12751280
}
1276-
return compileTestCase{entries: entries, expect: expect}
1281+
return compileTestCase{entries: entries, expect: expect, expectIsDefault: true}
1282+
}
1283+
1284+
func testcase_RedirectToDefaultResolverIsNotDefaultChain() compileTestCase {
1285+
entries := newEntries()
1286+
entries.AddResolvers(
1287+
&structs.ServiceResolverConfigEntry{
1288+
Kind: structs.ServiceResolver,
1289+
Name: "main",
1290+
Redirect: &structs.ServiceResolverRedirect{
1291+
Service: "other",
1292+
},
1293+
},
1294+
)
1295+
1296+
resolver := newDefaultServiceResolver("other")
1297+
1298+
expect := &structs.CompiledDiscoveryChain{
1299+
Protocol: "tcp",
1300+
Node: &structs.DiscoveryGraphNode{
1301+
Type: structs.DiscoveryGraphNodeTypeGroupResolver,
1302+
Name: "other",
1303+
GroupResolver: &structs.DiscoveryGroupResolver{
1304+
Definition: resolver,
1305+
Default: true,
1306+
ConnectTimeout: 5 * time.Second,
1307+
Target: newTarget("other", "", "default", "dc1"),
1308+
},
1309+
},
1310+
Resolvers: map[string]*structs.ServiceResolverConfigEntry{
1311+
"other": resolver,
1312+
},
1313+
Targets: []structs.DiscoveryTarget{
1314+
newTarget("other", "", "default", "dc1"),
1315+
},
1316+
GroupResolverNodes: map[structs.DiscoveryTarget]*structs.DiscoveryGraphNode{
1317+
newTarget("other", "", "default", "dc1"): nil,
1318+
},
1319+
}
1320+
1321+
return compileTestCase{entries: entries, expect: expect, expectIsDefault: false /*being explicit here because this is the whole point of this test*/}
12771322
}
12781323

12791324
func testcase_Resolve_WithDefaultSubset() compileTestCase {

agent/http_oss.go

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ func init() {
8888
registerEndpoint("/v1/health/state/", []string{"GET"}, (*HTTPServer).HealthChecksInState)
8989
registerEndpoint("/v1/health/service/", []string{"GET"}, (*HTTPServer).HealthServiceNodes)
9090
registerEndpoint("/v1/health/connect/", []string{"GET"}, (*HTTPServer).HealthConnectServiceNodes)
91+
registerEndpoint("/v1/internal/discovery-chain/", []string{"GET"}, (*HTTPServer).InternalDiscoveryChain)
9192
registerEndpoint("/v1/internal/ui/nodes", []string{"GET"}, (*HTTPServer).UINodes)
9293
registerEndpoint("/v1/internal/ui/node/", []string{"GET"}, (*HTTPServer).UINodeInfo)
9394
registerEndpoint("/v1/internal/ui/services", []string{"GET"}, (*HTTPServer).UIServices)

agent/internal_endpoint.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package agent
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/hashicorp/consul/agent/structs"
9+
)
10+
11+
// InternalDiscoveryChain is helpful for debugging. Eventually we should expose
12+
// this data officially somehow.
13+
func (s *HTTPServer) InternalDiscoveryChain(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
14+
var args structs.DiscoveryChainRequest
15+
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
16+
return nil, nil
17+
}
18+
19+
args.Name = strings.TrimPrefix(req.URL.Path, "/v1/internal/discovery-chain/")
20+
if args.Name == "" {
21+
resp.WriteHeader(http.StatusBadRequest)
22+
fmt.Fprint(resp, "Missing chain name")
23+
return nil, nil
24+
}
25+
26+
// Make the RPC request
27+
var out structs.DiscoveryChainResponse
28+
defer setMeta(resp, &out.QueryMeta)
29+
30+
if err := s.agent.RPC("ConfigEntry.ReadDiscoveryChain", &args, &out); err != nil {
31+
return nil, err
32+
}
33+
34+
if out.Chain == nil {
35+
resp.WriteHeader(http.StatusNotFound)
36+
return nil, nil
37+
}
38+
39+
return out.Chain, nil
40+
}

agent/structs/discovery_chain.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,18 @@ type CompiledDiscoveryChain struct {
4040
Targets []DiscoveryTarget `json:",omitempty"`
4141
}
4242

43+
// IsDefault returns true if the compiled chain represents no routing, no
44+
// splitting, and only the default resolution. We have to be careful here to
45+
// avoid returning "yep this is default" when the only resolver action being
46+
// applied is redirection to another resolver that is default, so we double
47+
// check the resolver matches the requested resolver.
4348
func (c *CompiledDiscoveryChain) IsDefault() bool {
4449
if c.Node == nil {
4550
return true
4651
}
47-
return c.Node.Type == DiscoveryGraphNodeTypeGroupResolver && c.Node.GroupResolver.Default
52+
return c.Node.Name == c.ServiceName &&
53+
c.Node.Type == DiscoveryGraphNodeTypeGroupResolver &&
54+
c.Node.GroupResolver.Default
4855
}
4956

5057
const (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
enable_central_service_config = true
2+
3+
config_entries {
4+
bootstrap {
5+
kind = "proxy-defaults"
6+
name = "global"
7+
8+
config {
9+
protocol = "http"
10+
}
11+
}
12+
13+
bootstrap {
14+
kind = "service-resolver"
15+
name = "s2"
16+
17+
redirect {
18+
service = "s3"
19+
}
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
services {
2+
name = "s3"
3+
port = 8282
4+
connect { sidecar_service {} }
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
# retry because resolving the central config might race
6+
retry_default gen_envoy_bootstrap s1 19000
7+
retry_default gen_envoy_bootstrap s2 19001
8+
retry_default gen_envoy_bootstrap s3 19002
9+
10+
export REQUIRED_SERVICES="s1 s1-sidecar-proxy s2 s2-sidecar-proxy s3 s3-sidecar-proxy"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env bats
2+
3+
load helpers
4+
5+
@test "s1 proxy admin is up on :19000" {
6+
retry_default curl -f -s localhost:19000/stats -o /dev/null
7+
}
8+
9+
@test "s2 proxy admin is up on :19001" {
10+
retry_default curl -f -s localhost:19001/stats -o /dev/null
11+
}
12+
13+
@test "s3 proxy admin is up on :19002" {
14+
retry_default curl -f -s localhost:19002/stats -o /dev/null
15+
}
16+
17+
@test "s1 proxy listener should be up and have right cert" {
18+
assert_proxy_presents_cert_uri localhost:21000 s1
19+
}
20+
21+
@test "s2 proxy listener should be up and have right cert" {
22+
assert_proxy_presents_cert_uri localhost:21001 s2
23+
}
24+
25+
@test "s3 proxy listener should be up and have right cert" {
26+
assert_proxy_presents_cert_uri localhost:21002 s3
27+
}
28+
29+
@test "s3 proxy should be healthy" {
30+
assert_service_has_healthy_instances s3 1
31+
}
32+
33+
@test "s1 upstream should have healthy endpoints for s3" {
34+
assert_upstream_has_healthy_endpoints 127.0.0.1:19000 s3 1
35+
}
36+
37+
@test "s1 upstream should be able to connect to its upstream simply" {
38+
run retry_default curl -s -f -d hello localhost:5000
39+
[ "$status" -eq 0 ]
40+
[ "$output" = "hello" ]
41+
}
42+
43+
@test "s1 upstream should be able to connect to s3 via upstream s2" {
44+
assert_expected_fortio_name s3
45+
}
46+

test/integration/connect/envoy/docker-compose.yml

+45
Original file line numberDiff line numberDiff line change
@@ -46,24 +46,48 @@ services:
4646
depends_on:
4747
- consul
4848
image: "fortio/fortio"
49+
environment:
50+
- "FORTIO_NAME=s1"
4951
command:
5052
- "server"
5153
- "-http-port"
5254
- ":8080"
5355
- "-grpc-port"
5456
- ":8079"
57+
- "-redirect-port"
58+
- "disabled"
5559
network_mode: service:consul
5660

5761
s2:
5862
depends_on:
5963
- consul
6064
image: "fortio/fortio"
65+
environment:
66+
- "FORTIO_NAME=s2"
6167
command:
6268
- "server"
6369
- "-http-port"
6470
- ":8181"
6571
- "-grpc-port"
6672
- ":8179"
73+
- "-redirect-port"
74+
- "disabled"
75+
network_mode: service:consul
76+
77+
s3:
78+
depends_on:
79+
- consul
80+
image: "fortio/fortio"
81+
environment:
82+
- "FORTIO_NAME=s3"
83+
command:
84+
- "server"
85+
- "-http-port"
86+
- ":8282"
87+
- "-grpc-port"
88+
- ":8279"
89+
- "-redirect-port"
90+
- "disabled"
6791
network_mode: service:consul
6892

6993
s1-sidecar-proxy:
@@ -108,6 +132,27 @@ services:
108132
- *workdir-volume
109133
network_mode: service:consul
110134

135+
s3-sidecar-proxy:
136+
depends_on:
137+
- consul
138+
image: "envoyproxy/envoy:v${ENVOY_VERSION:-1.8.0}"
139+
command:
140+
- "envoy"
141+
- "-c"
142+
- "/workdir/envoy/s3-bootstrap.json"
143+
- "-l"
144+
- "debug"
145+
# Hot restart breaks since both envoys seem to interact with each other
146+
# despite separate containers that don't share IPC namespace. Not quite
147+
# sure how this happens but may be due to unix socket being in some shared
148+
# location?
149+
- "--disable-hot-restart"
150+
- "--drain-time-s"
151+
- "1"
152+
volumes:
153+
- *workdir-volume
154+
network_mode: service:consul
155+
111156
verify:
112157
depends_on:
113158
- consul

test/integration/connect/envoy/helpers.bash

+15
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,18 @@ function gen_envoy_bootstrap {
253253
return $status
254254
fi
255255
}
256+
257+
function get_upstream_fortio_name {
258+
run retry_default curl -v -s -f localhost:5000/debug?env=dump
259+
[ "$status" == 0 ]
260+
echo "$output" | grep -E "^FORTIO_NAME="
261+
}
262+
263+
function assert_expected_fortio_name {
264+
local EXPECT_NAME=$1
265+
266+
GOT=$(get_upstream_fortio_name)
267+
echo "GOT $GOT"
268+
269+
[ "$GOT" == "FORTIO_NAME=${EXPECT_NAME}" ]
270+
}

0 commit comments

Comments
 (0)