xds: Client refactor in preparation for xDS v3 support (#3743)

This commit is contained in:
Easwar Swaminathan 2020-07-30 10:27:09 -07:00 committed by GitHub
parent d6c4e49aab
commit 97c30a1419
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 4047 additions and 3180 deletions

View File

@ -25,7 +25,6 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"google.golang.org/grpc/balancer" "google.golang.org/grpc/balancer"
"google.golang.org/grpc/connectivity" "google.golang.org/grpc/connectivity"
xdsclient "google.golang.org/grpc/xds/internal/client"
"google.golang.org/grpc/xds/internal/testutils" "google.golang.org/grpc/xds/internal/testutils"
) )
@ -39,10 +38,10 @@ func (s) TestEDSPriority_HighPriorityReady(t *testing.T) {
edsb.enqueueChildBalancerStateUpdate = edsb.updateState edsb.enqueueChildBalancerStateUpdate = edsb.updateState
// Two localities, with priorities [0, 1], each with one backend. // Two localities, with priorities [0, 1], each with one backend.
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
addrs1 := <-cc.NewSubConnAddrsCh addrs1 := <-cc.NewSubConnAddrsCh
if got, want := addrs1[0].Addr, testEndpointAddrs[0]; got != want { if got, want := addrs1[0].Addr, testEndpointAddrs[0]; got != want {
@ -62,11 +61,11 @@ func (s) TestEDSPriority_HighPriorityReady(t *testing.T) {
} }
// Add p2, it shouldn't cause any udpates. // Add p2, it shouldn't cause any udpates.
clab2 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab2 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab2.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab2.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab2.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab2.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
clab2.AddLocality(testSubZones[2], 1, 2, testEndpointAddrs[2:3], nil) clab2.AddLocality(testSubZones[2], 1, 2, testEndpointAddrs[2:3], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab2.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab2.Build()))
select { select {
case <-cc.NewPickerCh: case <-cc.NewPickerCh:
@ -79,10 +78,10 @@ func (s) TestEDSPriority_HighPriorityReady(t *testing.T) {
} }
// Remove p2, no updates. // Remove p2, no updates.
clab3 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab3 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab3.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab3.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab3.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab3.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab3.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab3.Build()))
select { select {
case <-cc.NewPickerCh: case <-cc.NewPickerCh:
@ -105,10 +104,10 @@ func (s) TestEDSPriority_SwitchPriority(t *testing.T) {
edsb.enqueueChildBalancerStateUpdate = edsb.updateState edsb.enqueueChildBalancerStateUpdate = edsb.updateState
// Two localities, with priorities [0, 1], each with one backend. // Two localities, with priorities [0, 1], each with one backend.
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
addrs0 := <-cc.NewSubConnAddrsCh addrs0 := <-cc.NewSubConnAddrsCh
if got, want := addrs0[0].Addr, testEndpointAddrs[0]; got != want { if got, want := addrs0[0].Addr, testEndpointAddrs[0]; got != want {
@ -147,11 +146,11 @@ func (s) TestEDSPriority_SwitchPriority(t *testing.T) {
} }
// Add p2, it shouldn't cause any udpates. // Add p2, it shouldn't cause any udpates.
clab2 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab2 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab2.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab2.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab2.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab2.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
clab2.AddLocality(testSubZones[2], 1, 2, testEndpointAddrs[2:3], nil) clab2.AddLocality(testSubZones[2], 1, 2, testEndpointAddrs[2:3], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab2.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab2.Build()))
select { select {
case <-cc.NewPickerCh: case <-cc.NewPickerCh:
@ -183,10 +182,10 @@ func (s) TestEDSPriority_SwitchPriority(t *testing.T) {
} }
// Remove 2, use 1. // Remove 2, use 1.
clab3 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab3 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab3.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab3.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab3.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab3.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab3.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab3.Build()))
// p2 SubConns are removed. // p2 SubConns are removed.
scToRemove := <-cc.RemoveSubConnCh scToRemove := <-cc.RemoveSubConnCh
@ -212,10 +211,10 @@ func (s) TestEDSPriority_HigherDownWhileAddingLower(t *testing.T) {
edsb.enqueueChildBalancerStateUpdate = edsb.updateState edsb.enqueueChildBalancerStateUpdate = edsb.updateState
// Two localities, with different priorities, each with one backend. // Two localities, with different priorities, each with one backend.
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
addrs0 := <-cc.NewSubConnAddrsCh addrs0 := <-cc.NewSubConnAddrsCh
if got, want := addrs0[0].Addr, testEndpointAddrs[0]; got != want { if got, want := addrs0[0].Addr, testEndpointAddrs[0]; got != want {
@ -242,11 +241,11 @@ func (s) TestEDSPriority_HigherDownWhileAddingLower(t *testing.T) {
} }
// Add p2, it should create a new SubConn. // Add p2, it should create a new SubConn.
clab2 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab2 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab2.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab2.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab2.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab2.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
clab2.AddLocality(testSubZones[2], 1, 2, testEndpointAddrs[2:3], nil) clab2.AddLocality(testSubZones[2], 1, 2, testEndpointAddrs[2:3], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab2.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab2.Build()))
addrs2 := <-cc.NewSubConnAddrsCh addrs2 := <-cc.NewSubConnAddrsCh
if got, want := addrs2[0].Addr, testEndpointAddrs[2]; got != want { if got, want := addrs2[0].Addr, testEndpointAddrs[2]; got != want {
@ -277,11 +276,11 @@ func (s) TestEDSPriority_HigherReadyCloseAllLower(t *testing.T) {
edsb.enqueueChildBalancerStateUpdate = edsb.updateState edsb.enqueueChildBalancerStateUpdate = edsb.updateState
// Two localities, with priorities [0,1,2], each with one backend. // Two localities, with priorities [0,1,2], each with one backend.
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
clab1.AddLocality(testSubZones[2], 1, 2, testEndpointAddrs[2:3], nil) clab1.AddLocality(testSubZones[2], 1, 2, testEndpointAddrs[2:3], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
addrs0 := <-cc.NewSubConnAddrsCh addrs0 := <-cc.NewSubConnAddrsCh
if got, want := addrs0[0].Addr, testEndpointAddrs[0]; got != want { if got, want := addrs0[0].Addr, testEndpointAddrs[0]; got != want {
@ -359,10 +358,10 @@ func (s) TestEDSPriority_InitTimeout(t *testing.T) {
edsb.enqueueChildBalancerStateUpdate = edsb.updateState edsb.enqueueChildBalancerStateUpdate = edsb.updateState
// Two localities, with different priorities, each with one backend. // Two localities, with different priorities, each with one backend.
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
addrs0 := <-cc.NewSubConnAddrsCh addrs0 := <-cc.NewSubConnAddrsCh
if got, want := addrs0[0].Addr, testEndpointAddrs[0]; got != want { if got, want := addrs0[0].Addr, testEndpointAddrs[0]; got != want {
@ -409,10 +408,10 @@ func (s) TestEDSPriority_MultipleLocalities(t *testing.T) {
edsb.enqueueChildBalancerStateUpdate = edsb.updateState edsb.enqueueChildBalancerStateUpdate = edsb.updateState
// Two localities, with different priorities, each with one backend. // Two localities, with different priorities, each with one backend.
clab0 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab0 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab0.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab0.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab0.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab0.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab0.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab0.Build()))
addrs0 := <-cc.NewSubConnAddrsCh addrs0 := <-cc.NewSubConnAddrsCh
if got, want := addrs0[0].Addr, testEndpointAddrs[0]; got != want { if got, want := addrs0[0].Addr, testEndpointAddrs[0]; got != want {
@ -463,12 +462,12 @@ func (s) TestEDSPriority_MultipleLocalities(t *testing.T) {
} }
// Add two localities, with two priorities, with one backend. // Add two localities, with two priorities, with one backend.
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
clab1.AddLocality(testSubZones[2], 1, 0, testEndpointAddrs[2:3], nil) clab1.AddLocality(testSubZones[2], 1, 0, testEndpointAddrs[2:3], nil)
clab1.AddLocality(testSubZones[3], 1, 1, testEndpointAddrs[3:4], nil) clab1.AddLocality(testSubZones[3], 1, 1, testEndpointAddrs[3:4], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
addrs2 := <-cc.NewSubConnAddrsCh addrs2 := <-cc.NewSubConnAddrsCh
if got, want := addrs2[0].Addr, testEndpointAddrs[2]; got != want { if got, want := addrs2[0].Addr, testEndpointAddrs[2]; got != want {
@ -520,10 +519,10 @@ func (s) TestEDSPriority_RemovesAllLocalities(t *testing.T) {
edsb.enqueueChildBalancerStateUpdate = edsb.updateState edsb.enqueueChildBalancerStateUpdate = edsb.updateState
// Two localities, with different priorities, each with one backend. // Two localities, with different priorities, each with one backend.
clab0 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab0 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab0.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab0.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab0.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab0.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab0.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab0.Build()))
addrs0 := <-cc.NewSubConnAddrsCh addrs0 := <-cc.NewSubConnAddrsCh
if got, want := addrs0[0].Addr, testEndpointAddrs[0]; got != want { if got, want := addrs0[0].Addr, testEndpointAddrs[0]; got != want {
@ -541,8 +540,8 @@ func (s) TestEDSPriority_RemovesAllLocalities(t *testing.T) {
} }
// Remove all priorities. // Remove all priorities.
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
// p0 subconn should be removed. // p0 subconn should be removed.
scToRemove := <-cc.RemoveSubConnCh scToRemove := <-cc.RemoveSubConnCh
@ -559,10 +558,10 @@ func (s) TestEDSPriority_RemovesAllLocalities(t *testing.T) {
} }
// Re-add two localities, with previous priorities, but different backends. // Re-add two localities, with previous priorities, but different backends.
clab2 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab2 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab2.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[2:3], nil) clab2.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[2:3], nil)
clab2.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[3:4], nil) clab2.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[3:4], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab2.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab2.Build()))
addrs01 := <-cc.NewSubConnAddrsCh addrs01 := <-cc.NewSubConnAddrsCh
if got, want := addrs01[0].Addr, testEndpointAddrs[2]; got != want { if got, want := addrs01[0].Addr, testEndpointAddrs[2]; got != want {
@ -591,9 +590,9 @@ func (s) TestEDSPriority_RemovesAllLocalities(t *testing.T) {
} }
// Remove p1 from EDS, to fallback to p0. // Remove p1 from EDS, to fallback to p0.
clab3 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab3 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab3.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[2:3], nil) clab3.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[2:3], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab3.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab3.Build()))
// p1 subconn should be removed. // p1 subconn should be removed.
scToRemove1 := <-cc.RemoveSubConnCh scToRemove1 := <-cc.RemoveSubConnCh
@ -664,10 +663,10 @@ func (s) TestEDSPriority_HighPriorityNoEndpoints(t *testing.T) {
edsb.enqueueChildBalancerStateUpdate = edsb.updateState edsb.enqueueChildBalancerStateUpdate = edsb.updateState
// Two localities, with priorities [0, 1], each with one backend. // Two localities, with priorities [0, 1], each with one backend.
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
addrs1 := <-cc.NewSubConnAddrsCh addrs1 := <-cc.NewSubConnAddrsCh
if got, want := addrs1[0].Addr, testEndpointAddrs[0]; got != want { if got, want := addrs1[0].Addr, testEndpointAddrs[0]; got != want {
@ -687,10 +686,10 @@ func (s) TestEDSPriority_HighPriorityNoEndpoints(t *testing.T) {
} }
// Remove addresses from priority 0, should use p1. // Remove addresses from priority 0, should use p1.
clab2 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab2 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab2.AddLocality(testSubZones[0], 1, 0, nil, nil) clab2.AddLocality(testSubZones[0], 1, 0, nil, nil)
clab2.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab2.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab2.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab2.Build()))
// p0 will remove the subconn, and ClientConn will send a sc update to // p0 will remove the subconn, and ClientConn will send a sc update to
// shutdown. // shutdown.
@ -723,10 +722,10 @@ func (s) TestEDSPriority_HighPriorityAllUnhealthy(t *testing.T) {
edsb.enqueueChildBalancerStateUpdate = edsb.updateState edsb.enqueueChildBalancerStateUpdate = edsb.updateState
// Two localities, with priorities [0, 1], each with one backend. // Two localities, with priorities [0, 1], each with one backend.
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab1.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
addrs1 := <-cc.NewSubConnAddrsCh addrs1 := <-cc.NewSubConnAddrsCh
if got, want := addrs1[0].Addr, testEndpointAddrs[0]; got != want { if got, want := addrs1[0].Addr, testEndpointAddrs[0]; got != want {
@ -746,12 +745,12 @@ func (s) TestEDSPriority_HighPriorityAllUnhealthy(t *testing.T) {
} }
// Set priority 0 endpoints to all unhealthy, should use p1. // Set priority 0 endpoints to all unhealthy, should use p1.
clab2 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab2 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab2.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], &xdsclient.AddLocalityOptions{ clab2.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], &testutils.AddLocalityOptions{
Health: []corepb.HealthStatus{corepb.HealthStatus_UNHEALTHY}, Health: []corepb.HealthStatus{corepb.HealthStatus_UNHEALTHY},
}) })
clab2.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil) clab2.AddLocality(testSubZones[1], 1, 1, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab2.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab2.Build()))
// p0 will remove the subconn, and ClientConn will send a sc update to // p0 will remove the subconn, and ClientConn will send a sc update to
// transient failure. // transient failure.

View File

@ -60,9 +60,9 @@ func (s) TestEDS_OneLocality(t *testing.T) {
edsb.enqueueChildBalancerStateUpdate = edsb.updateState edsb.enqueueChildBalancerStateUpdate = edsb.updateState
// One locality with one backend. // One locality with one backend.
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
sc1 := <-cc.NewSubConnCh sc1 := <-cc.NewSubConnCh
edsb.handleSubConnStateChange(sc1, connectivity.Connecting) edsb.handleSubConnStateChange(sc1, connectivity.Connecting)
@ -78,9 +78,9 @@ func (s) TestEDS_OneLocality(t *testing.T) {
} }
// The same locality, add one more backend. // The same locality, add one more backend.
clab2 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab2 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab2.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:2], nil) clab2.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab2.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab2.Build()))
sc2 := <-cc.NewSubConnCh sc2 := <-cc.NewSubConnCh
edsb.handleSubConnStateChange(sc2, connectivity.Connecting) edsb.handleSubConnStateChange(sc2, connectivity.Connecting)
@ -94,9 +94,9 @@ func (s) TestEDS_OneLocality(t *testing.T) {
} }
// The same locality, delete first backend. // The same locality, delete first backend.
clab3 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab3 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab3.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[1:2], nil) clab3.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab3.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab3.Build()))
scToRemove := <-cc.RemoveSubConnCh scToRemove := <-cc.RemoveSubConnCh
if !cmp.Equal(scToRemove, sc1, cmp.AllowUnexported(testutils.TestSubConn{})) { if !cmp.Equal(scToRemove, sc1, cmp.AllowUnexported(testutils.TestSubConn{})) {
@ -114,9 +114,9 @@ func (s) TestEDS_OneLocality(t *testing.T) {
} }
// The same locality, replace backend. // The same locality, replace backend.
clab4 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab4 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab4.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[2:3], nil) clab4.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[2:3], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab4.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab4.Build()))
sc3 := <-cc.NewSubConnCh sc3 := <-cc.NewSubConnCh
edsb.handleSubConnStateChange(sc3, connectivity.Connecting) edsb.handleSubConnStateChange(sc3, connectivity.Connecting)
@ -137,9 +137,9 @@ func (s) TestEDS_OneLocality(t *testing.T) {
} }
// The same locality, different drop rate, dropping 50%. // The same locality, different drop rate, dropping 50%.
clab5 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], []uint32{50}) clab5 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], []uint32{50})
clab5.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[2:3], nil) clab5.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[2:3], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab5.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab5.Build()))
// Picks with drops. // Picks with drops.
p5 := <-cc.NewPickerCh p5 := <-cc.NewPickerCh
@ -155,9 +155,9 @@ func (s) TestEDS_OneLocality(t *testing.T) {
} }
// The same locality, remove drops. // The same locality, remove drops.
clab6 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab6 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab6.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[2:3], nil) clab6.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[2:3], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab6.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab6.Build()))
// Pick without drops. // Pick without drops.
p6 := <-cc.NewPickerCh p6 := <-cc.NewPickerCh
@ -181,9 +181,9 @@ func (s) TestEDS_TwoLocalities(t *testing.T) {
edsb.enqueueChildBalancerStateUpdate = edsb.updateState edsb.enqueueChildBalancerStateUpdate = edsb.updateState
// Two localities, each with one backend. // Two localities, each with one backend.
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
sc1 := <-cc.NewSubConnCh sc1 := <-cc.NewSubConnCh
edsb.handleSubConnStateChange(sc1, connectivity.Connecting) edsb.handleSubConnStateChange(sc1, connectivity.Connecting)
edsb.handleSubConnStateChange(sc1, connectivity.Ready) edsb.handleSubConnStateChange(sc1, connectivity.Ready)
@ -192,7 +192,7 @@ func (s) TestEDS_TwoLocalities(t *testing.T) {
// locality. Otherwise the test is flaky because of a map is used in EDS to // locality. Otherwise the test is flaky because of a map is used in EDS to
// keep localities. // keep localities.
clab1.AddLocality(testSubZones[1], 1, 0, testEndpointAddrs[1:2], nil) clab1.AddLocality(testSubZones[1], 1, 0, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
sc2 := <-cc.NewSubConnCh sc2 := <-cc.NewSubConnCh
edsb.handleSubConnStateChange(sc2, connectivity.Connecting) edsb.handleSubConnStateChange(sc2, connectivity.Connecting)
edsb.handleSubConnStateChange(sc2, connectivity.Ready) edsb.handleSubConnStateChange(sc2, connectivity.Ready)
@ -205,11 +205,11 @@ func (s) TestEDS_TwoLocalities(t *testing.T) {
} }
// Add another locality, with one backend. // Add another locality, with one backend.
clab2 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab2 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab2.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab2.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab2.AddLocality(testSubZones[1], 1, 0, testEndpointAddrs[1:2], nil) clab2.AddLocality(testSubZones[1], 1, 0, testEndpointAddrs[1:2], nil)
clab2.AddLocality(testSubZones[2], 1, 0, testEndpointAddrs[2:3], nil) clab2.AddLocality(testSubZones[2], 1, 0, testEndpointAddrs[2:3], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab2.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab2.Build()))
sc3 := <-cc.NewSubConnCh sc3 := <-cc.NewSubConnCh
edsb.handleSubConnStateChange(sc3, connectivity.Connecting) edsb.handleSubConnStateChange(sc3, connectivity.Connecting)
@ -223,10 +223,10 @@ func (s) TestEDS_TwoLocalities(t *testing.T) {
} }
// Remove first locality. // Remove first locality.
clab3 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab3 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab3.AddLocality(testSubZones[1], 1, 0, testEndpointAddrs[1:2], nil) clab3.AddLocality(testSubZones[1], 1, 0, testEndpointAddrs[1:2], nil)
clab3.AddLocality(testSubZones[2], 1, 0, testEndpointAddrs[2:3], nil) clab3.AddLocality(testSubZones[2], 1, 0, testEndpointAddrs[2:3], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab3.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab3.Build()))
scToRemove := <-cc.RemoveSubConnCh scToRemove := <-cc.RemoveSubConnCh
if !cmp.Equal(scToRemove, sc1, cmp.AllowUnexported(testutils.TestSubConn{})) { if !cmp.Equal(scToRemove, sc1, cmp.AllowUnexported(testutils.TestSubConn{})) {
@ -242,10 +242,10 @@ func (s) TestEDS_TwoLocalities(t *testing.T) {
} }
// Add a backend to the last locality. // Add a backend to the last locality.
clab4 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab4 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab4.AddLocality(testSubZones[1], 1, 0, testEndpointAddrs[1:2], nil) clab4.AddLocality(testSubZones[1], 1, 0, testEndpointAddrs[1:2], nil)
clab4.AddLocality(testSubZones[2], 1, 0, testEndpointAddrs[2:4], nil) clab4.AddLocality(testSubZones[2], 1, 0, testEndpointAddrs[2:4], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab4.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab4.Build()))
sc4 := <-cc.NewSubConnCh sc4 := <-cc.NewSubConnCh
edsb.handleSubConnStateChange(sc4, connectivity.Connecting) edsb.handleSubConnStateChange(sc4, connectivity.Connecting)
@ -262,10 +262,10 @@ func (s) TestEDS_TwoLocalities(t *testing.T) {
} }
// Change weight of the locality[1]. // Change weight of the locality[1].
clab5 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab5 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab5.AddLocality(testSubZones[1], 2, 0, testEndpointAddrs[1:2], nil) clab5.AddLocality(testSubZones[1], 2, 0, testEndpointAddrs[1:2], nil)
clab5.AddLocality(testSubZones[2], 1, 0, testEndpointAddrs[2:4], nil) clab5.AddLocality(testSubZones[2], 1, 0, testEndpointAddrs[2:4], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab5.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab5.Build()))
// Test pick with two subconns different locality weight. // Test pick with two subconns different locality weight.
p5 := <-cc.NewPickerCh p5 := <-cc.NewPickerCh
@ -278,10 +278,10 @@ func (s) TestEDS_TwoLocalities(t *testing.T) {
} }
// Change weight of the locality[1] to 0, it should never be picked. // Change weight of the locality[1] to 0, it should never be picked.
clab6 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab6 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab6.AddLocality(testSubZones[1], 0, 0, testEndpointAddrs[1:2], nil) clab6.AddLocality(testSubZones[1], 0, 0, testEndpointAddrs[1:2], nil)
clab6.AddLocality(testSubZones[2], 1, 0, testEndpointAddrs[2:4], nil) clab6.AddLocality(testSubZones[2], 1, 0, testEndpointAddrs[2:4], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab6.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab6.Build()))
// Changing weight of locality[1] to 0 caused it to be removed. It's subconn // Changing weight of locality[1] to 0 caused it to be removed. It's subconn
// should also be removed. // should also be removed.
@ -312,8 +312,8 @@ func (s) TestEDS_EndpointsHealth(t *testing.T) {
edsb.enqueueChildBalancerStateUpdate = edsb.updateState edsb.enqueueChildBalancerStateUpdate = edsb.updateState
// Two localities, each 3 backend, one Healthy, one Unhealthy, one Unknown. // Two localities, each 3 backend, one Healthy, one Unhealthy, one Unknown.
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:6], &xdsclient.AddLocalityOptions{ clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:6], &testutils.AddLocalityOptions{
Health: []corepb.HealthStatus{ Health: []corepb.HealthStatus{
corepb.HealthStatus_HEALTHY, corepb.HealthStatus_HEALTHY,
corepb.HealthStatus_UNHEALTHY, corepb.HealthStatus_UNHEALTHY,
@ -323,7 +323,7 @@ func (s) TestEDS_EndpointsHealth(t *testing.T) {
corepb.HealthStatus_DEGRADED, corepb.HealthStatus_DEGRADED,
}, },
}) })
clab1.AddLocality(testSubZones[1], 1, 0, testEndpointAddrs[6:12], &xdsclient.AddLocalityOptions{ clab1.AddLocality(testSubZones[1], 1, 0, testEndpointAddrs[6:12], &testutils.AddLocalityOptions{
Health: []corepb.HealthStatus{ Health: []corepb.HealthStatus{
corepb.HealthStatus_HEALTHY, corepb.HealthStatus_HEALTHY,
corepb.HealthStatus_UNHEALTHY, corepb.HealthStatus_UNHEALTHY,
@ -333,7 +333,7 @@ func (s) TestEDS_EndpointsHealth(t *testing.T) {
corepb.HealthStatus_DEGRADED, corepb.HealthStatus_DEGRADED,
}, },
}) })
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
var ( var (
readySCs []balancer.SubConn readySCs []balancer.SubConn
@ -406,9 +406,9 @@ func (s) TestEDS_EmptyUpdate(t *testing.T) {
} }
// One locality with one backend. // One locality with one backend.
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
sc1 := <-cc.NewSubConnCh sc1 := <-cc.NewSubConnCh
edsb.handleSubConnStateChange(sc1, connectivity.Connecting) edsb.handleSubConnStateChange(sc1, connectivity.Connecting)
@ -434,7 +434,7 @@ func (s) TestEDS_EmptyUpdate(t *testing.T) {
} }
// Handle another update with priorities and localities. // Handle another update with priorities and localities.
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
sc2 := <-cc.NewSubConnCh sc2 := <-cc.NewSubConnCh
edsb.handleSubConnStateChange(sc2, connectivity.Connecting) edsb.handleSubConnStateChange(sc2, connectivity.Connecting)
@ -462,10 +462,10 @@ func (s) TestEDS_UpdateSubBalancerName(t *testing.T) {
edsb.handleChildPolicy("test-const-balancer", nil) edsb.handleChildPolicy("test-const-balancer", nil)
// Two localities, each with one backend. // Two localities, each with one backend.
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
clab1.AddLocality(testSubZones[1], 1, 0, testEndpointAddrs[1:2], nil) clab1.AddLocality(testSubZones[1], 1, 0, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
for i := 0; i < 2; i++ { for i := 0; i < 2; i++ {
sc := <-cc.NewSubConnCh sc := <-cc.NewSubConnCh
@ -601,9 +601,9 @@ func (s) TestEDS_ChildPolicyUpdatePickerInline(t *testing.T) {
edsb.handleChildPolicy("test-inline-update-balancer", nil) edsb.handleChildPolicy("test-inline-update-balancer", nil)
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
p0 := <-cc.NewPickerCh p0 := <-cc.NewPickerCh
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
@ -689,9 +689,9 @@ func (s) TestEDS_LoadReport(t *testing.T) {
backendToBalancerID := make(map[balancer.SubConn]internal.LocalityID) backendToBalancerID := make(map[balancer.SubConn]internal.LocalityID)
// Two localities, each with one backend. // Two localities, each with one backend.
clab1 := xdsclient.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil) clab1 := testutils.NewClusterLoadAssignmentBuilder(testClusterNames[0], nil)
clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil) clab1.AddLocality(testSubZones[0], 1, 0, testEndpointAddrs[:1], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
sc1 := <-cc.NewSubConnCh sc1 := <-cc.NewSubConnCh
edsb.handleSubConnStateChange(sc1, connectivity.Connecting) edsb.handleSubConnStateChange(sc1, connectivity.Connecting)
edsb.handleSubConnStateChange(sc1, connectivity.Ready) edsb.handleSubConnStateChange(sc1, connectivity.Ready)
@ -703,7 +703,7 @@ func (s) TestEDS_LoadReport(t *testing.T) {
// locality. Otherwise the test is flaky because of a map is used in EDS to // locality. Otherwise the test is flaky because of a map is used in EDS to
// keep localities. // keep localities.
clab1.AddLocality(testSubZones[1], 1, 0, testEndpointAddrs[1:2], nil) clab1.AddLocality(testSubZones[1], 1, 0, testEndpointAddrs[1:2], nil)
edsb.handleEDSResponse(xdsclient.ParseEDSRespProtoForTesting(clab1.Build())) edsb.handleEDSResponse(parseEDSRespProtoForTesting(clab1.Build()))
sc2 := <-cc.NewSubConnCh sc2 := <-cc.NewSubConnCh
edsb.handleSubConnStateChange(sc2, connectivity.Connecting) edsb.handleSubConnStateChange(sc2, connectivity.Connecting)
edsb.handleSubConnStateChange(sc2, connectivity.Ready) edsb.handleSubConnStateChange(sc2, connectivity.Ready)

View File

@ -41,6 +41,8 @@ import (
"google.golang.org/grpc/xds/internal/client/bootstrap" "google.golang.org/grpc/xds/internal/client/bootstrap"
"google.golang.org/grpc/xds/internal/testutils" "google.golang.org/grpc/xds/internal/testutils"
"google.golang.org/grpc/xds/internal/testutils/fakeclient" "google.golang.org/grpc/xds/internal/testutils/fakeclient"
_ "google.golang.org/grpc/xds/internal/client/v2" // V2 client registration.
) )
func init() { func init() {

View File

@ -1,6 +1,5 @@
/* /*
* * Copyright 2020 gRPC authors.
* Copyright 2019 gRPC authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -15,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package client package edsbalancer
import ( import (
"fmt" "fmt"
@ -26,15 +25,61 @@ import (
corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
endpointpb "github.com/envoyproxy/go-control-plane/envoy/api/v2/endpoint" endpointpb "github.com/envoyproxy/go-control-plane/envoy/api/v2/endpoint"
typepb "github.com/envoyproxy/go-control-plane/envoy/type" typepb "github.com/envoyproxy/go-control-plane/envoy/type"
"github.com/golang/protobuf/ptypes"
"google.golang.org/grpc/xds/internal" "google.golang.org/grpc/xds/internal"
xdsclient "google.golang.org/grpc/xds/internal/client"
) )
// parseEDSRespProtoForTesting parses EDS response, and panic if parsing fails.
//
// TODO: delete this. The EDS balancer tests should build an EndpointsUpdate
// directly, instead of building and parsing a proto message.
func parseEDSRespProtoForTesting(m *xdspb.ClusterLoadAssignment) xdsclient.EndpointsUpdate {
u, err := parseEDSRespProto(m)
if err != nil {
panic(err.Error())
}
return u
}
// parseEDSRespProto turns EDS response proto message to EndpointsUpdate.
func parseEDSRespProto(m *xdspb.ClusterLoadAssignment) (xdsclient.EndpointsUpdate, error) {
ret := xdsclient.EndpointsUpdate{}
for _, dropPolicy := range m.GetPolicy().GetDropOverloads() {
ret.Drops = append(ret.Drops, parseDropPolicy(dropPolicy))
}
priorities := make(map[uint32]struct{})
for _, locality := range m.Endpoints {
l := locality.GetLocality()
if l == nil {
return xdsclient.EndpointsUpdate{}, fmt.Errorf("EDS response contains a locality without ID, locality: %+v", locality)
}
lid := internal.LocalityID{
Region: l.Region,
Zone: l.Zone,
SubZone: l.SubZone,
}
priority := locality.GetPriority()
priorities[priority] = struct{}{}
ret.Localities = append(ret.Localities, xdsclient.Locality{
ID: lid,
Endpoints: parseEndpoints(locality.GetLbEndpoints()),
Weight: locality.GetLoadBalancingWeight().GetValue(),
Priority: priority,
})
}
for i := 0; i < len(priorities); i++ {
if _, ok := priorities[uint32(i)]; !ok {
return xdsclient.EndpointsUpdate{}, fmt.Errorf("priority %v missing (with different priorities %v received)", i, priorities)
}
}
return ret, nil
}
func parseAddress(socketAddress *corepb.SocketAddress) string { func parseAddress(socketAddress *corepb.SocketAddress) string {
return net.JoinHostPort(socketAddress.GetAddress(), strconv.Itoa(int(socketAddress.GetPortValue()))) return net.JoinHostPort(socketAddress.GetAddress(), strconv.Itoa(int(socketAddress.GetPortValue())))
} }
func parseDropPolicy(dropPolicy *xdspb.ClusterLoadAssignment_Policy_DropOverload) OverloadDropConfig { func parseDropPolicy(dropPolicy *xdspb.ClusterLoadAssignment_Policy_DropOverload) xdsclient.OverloadDropConfig {
percentage := dropPolicy.GetDropPercentage() percentage := dropPolicy.GetDropPercentage()
var ( var (
numerator = percentage.GetNumerator() numerator = percentage.GetNumerator()
@ -48,96 +93,21 @@ func parseDropPolicy(dropPolicy *xdspb.ClusterLoadAssignment_Policy_DropOverload
case typepb.FractionalPercent_MILLION: case typepb.FractionalPercent_MILLION:
denominator = 1000000 denominator = 1000000
} }
return OverloadDropConfig{ return xdsclient.OverloadDropConfig{
Category: dropPolicy.GetCategory(), Category: dropPolicy.GetCategory(),
Numerator: numerator, Numerator: numerator,
Denominator: denominator, Denominator: denominator,
} }
} }
func parseEndpoints(lbEndpoints []*endpointpb.LbEndpoint) []Endpoint { func parseEndpoints(lbEndpoints []*endpointpb.LbEndpoint) []xdsclient.Endpoint {
endpoints := make([]Endpoint, 0, len(lbEndpoints)) endpoints := make([]xdsclient.Endpoint, 0, len(lbEndpoints))
for _, lbEndpoint := range lbEndpoints { for _, lbEndpoint := range lbEndpoints {
endpoints = append(endpoints, Endpoint{ endpoints = append(endpoints, xdsclient.Endpoint{
HealthStatus: EndpointHealthStatus(lbEndpoint.GetHealthStatus()), HealthStatus: xdsclient.EndpointHealthStatus(lbEndpoint.GetHealthStatus()),
Address: parseAddress(lbEndpoint.GetEndpoint().GetAddress().GetSocketAddress()), Address: parseAddress(lbEndpoint.GetEndpoint().GetAddress().GetSocketAddress()),
Weight: lbEndpoint.GetLoadBalancingWeight().GetValue(), Weight: lbEndpoint.GetLoadBalancingWeight().GetValue(),
}) })
} }
return endpoints return endpoints
} }
// ParseEDSRespProto turns EDS response proto message to EndpointsUpdate.
//
// This is temporarily exported to be used in eds balancer, before it switches
// to use xds client. TODO: unexport.
func ParseEDSRespProto(m *xdspb.ClusterLoadAssignment) (EndpointsUpdate, error) {
ret := EndpointsUpdate{}
for _, dropPolicy := range m.GetPolicy().GetDropOverloads() {
ret.Drops = append(ret.Drops, parseDropPolicy(dropPolicy))
}
priorities := make(map[uint32]struct{})
for _, locality := range m.Endpoints {
l := locality.GetLocality()
if l == nil {
return EndpointsUpdate{}, fmt.Errorf("EDS response contains a locality without ID, locality: %+v", locality)
}
lid := internal.LocalityID{
Region: l.Region,
Zone: l.Zone,
SubZone: l.SubZone,
}
priority := locality.GetPriority()
priorities[priority] = struct{}{}
ret.Localities = append(ret.Localities, Locality{
ID: lid,
Endpoints: parseEndpoints(locality.GetLbEndpoints()),
Weight: locality.GetLoadBalancingWeight().GetValue(),
Priority: priority,
})
}
for i := 0; i < len(priorities); i++ {
if _, ok := priorities[uint32(i)]; !ok {
return EndpointsUpdate{}, fmt.Errorf("priority %v missing (with different priorities %v received)", i, priorities)
}
}
return ret, nil
}
// ParseEDSRespProtoForTesting parses EDS response, and panic if parsing fails.
// This is used by EDS balancer tests.
//
// TODO: delete this. The EDS balancer tests should build an EndpointsUpdate directly,
// instead of building and parsing a proto message.
func ParseEDSRespProtoForTesting(m *xdspb.ClusterLoadAssignment) EndpointsUpdate {
u, err := ParseEDSRespProto(m)
if err != nil {
panic(err.Error())
}
return u
}
func (v2c *v2Client) handleEDSResponse(resp *xdspb.DiscoveryResponse) error {
returnUpdate := make(map[string]EndpointsUpdate)
for _, r := range resp.GetResources() {
var resource ptypes.DynamicAny
if err := ptypes.UnmarshalAny(r, &resource); err != nil {
return fmt.Errorf("xds: failed to unmarshal resource in EDS response: %v", err)
}
cla, ok := resource.Message.(*xdspb.ClusterLoadAssignment)
if !ok {
return fmt.Errorf("xds: unexpected resource type: %T in EDS response", resource.Message)
}
v2c.logger.Infof("Resource with name: %v, type: %T, contains: %v", cla.GetClusterName(), cla, cla)
u, err := ParseEDSRespProto(cla)
if err != nil {
return err
}
returnUpdate[cla.GetClusterName()] = u
}
v2c.parent.newEDSUpdate(returnUpdate)
return nil
}

View File

@ -36,10 +36,7 @@ import (
"google.golang.org/grpc/xds/internal/testutils" "google.golang.org/grpc/xds/internal/testutils"
"google.golang.org/grpc/xds/internal/testutils/fakeclient" "google.golang.org/grpc/xds/internal/testutils/fakeclient"
"google.golang.org/grpc/xds/internal/testutils/fakeserver" "google.golang.org/grpc/xds/internal/testutils/fakeserver"
) "google.golang.org/grpc/xds/internal/version"
const (
edsType = "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment"
) )
var ( var (
@ -135,7 +132,7 @@ func (s) TestClientWrapperWatchEDS(t *testing.T) {
} }
wantReq := &xdspb.DiscoveryRequest{ wantReq := &xdspb.DiscoveryRequest{
TypeUrl: edsType, TypeUrl: version.V2EndpointsURL,
ResourceNames: []string{test.wantResourceName}, ResourceNames: []string{test.wantResourceName},
Node: testutils.EmptyNodeProtoV2, Node: testutils.EmptyNodeProtoV2,
} }

View File

@ -26,17 +26,204 @@ import (
"sync" "sync"
"time" "time"
corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" v2corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
"github.com/golang/protobuf/proto"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/internal/backoff" "google.golang.org/grpc/internal/backoff"
"google.golang.org/grpc/internal/buffer" "google.golang.org/grpc/internal/buffer"
"google.golang.org/grpc/internal/grpclog" "google.golang.org/grpc/internal/grpclog"
"google.golang.org/grpc/internal/grpcsync" "google.golang.org/grpc/internal/grpcsync"
"google.golang.org/grpc/keepalive" "google.golang.org/grpc/keepalive"
"google.golang.org/grpc/xds/internal"
"google.golang.org/grpc/xds/internal/client/bootstrap" "google.golang.org/grpc/xds/internal/client/bootstrap"
"google.golang.org/grpc/xds/internal/version" "google.golang.org/grpc/xds/internal/version"
) )
var (
m = make(map[version.TransportAPI]APIClientBuilder)
)
// RegisterAPIClientBuilder registers a client builder for xDS transport protocol
// version specified by b.Version().
//
// NOTE: this function must only be called during initialization time (i.e. in
// an init() function), and is not thread-safe. If multiple builders are
// registered for the same version, the one registered last will take effect.
func RegisterAPIClientBuilder(b APIClientBuilder) {
m[b.Version()] = b
}
// getAPIClientBuilder returns the client builder registered for the provided
// xDS transport API version.
func getAPIClientBuilder(version version.TransportAPI) APIClientBuilder {
if b, ok := m[version]; ok {
return b
}
return nil
}
// BuildOptions contains options to be passed to client builders.
type BuildOptions struct {
// Parent is a top-level xDS client or server which has the intelligence to
// take appropriate action based on xDS responses received from the
// management server.
Parent UpdateHandler
// NodeProto contains the Node proto to be used in xDS requests. The actual
// type depends on the transport protocol version used.
NodeProto proto.Message
// Backoff returns the amount of time to backoff before retrying broken
// streams.
Backoff func(int) time.Duration
// Logger provides enhanced logging capabilities.
Logger *grpclog.PrefixLogger
}
// APIClientBuilder creates an xDS client for a specific xDS transport protocol
// version.
type APIClientBuilder interface {
// Build builds a transport protocol specific implementation of the xDS
// client based on the provided clientConn to the management server and the
// provided options.
Build(*grpc.ClientConn, BuildOptions) (APIClient, error)
// Version returns the xDS transport protocol version used by clients build
// using this builder.
Version() version.TransportAPI
}
// APIClient represents the functionality provided by transport protocol
// version specific implementations of the xDS client.
type APIClient interface {
// AddWatch adds a watch for an xDS resource given its type and name.
AddWatch(resourceType, resourceName string)
// RemoveWatch cancels an already registered watch for an xDS resource
// given its type and name.
RemoveWatch(resourceType, resourceName string)
// Close cleans up resources allocated by the API client.
Close()
}
// UpdateHandler receives and processes (by taking appropriate actions) xDS
// resource updates from an APIClient for a specific version.
type UpdateHandler interface {
// NewListeners handles updates to xDS listener resources.
NewListeners(map[string]ListenerUpdate)
// NewRouteConfigs handles updates to xDS RouteConfiguration resources.
NewRouteConfigs(map[string]RouteConfigUpdate)
// NewClusters handles updates to xDS Cluster resources.
NewClusters(map[string]ClusterUpdate)
// NewEndpoints handles updates to xDS ClusterLoadAssignment (or tersely
// referred to as Endpoints) resources.
NewEndpoints(map[string]EndpointsUpdate)
}
// ListenerUpdate contains information received in an LDS response, which is of
// interest to the registered LDS watcher.
type ListenerUpdate struct {
// RouteConfigName is the route configuration name corresponding to the
// target which is being watched through LDS.
RouteConfigName string
}
// RouteConfigUpdate contains information received in an RDS response, which is
// of interest to the registered RDS watcher.
type RouteConfigUpdate struct {
// Routes contains a list of routes, each containing matchers and
// corresponding action.
Routes []*Route
}
// Route is both a specification of how to match a request as well as an
// indication of the action to take upon match.
type Route struct {
Path, Prefix, Regex *string
Headers []*HeaderMatcher
Fraction *uint32
Action map[string]uint32 // action is weighted clusters.
}
// HeaderMatcher represents header matchers.
type HeaderMatcher struct {
Name string `json:"name"`
InvertMatch *bool `json:"invertMatch,omitempty"`
ExactMatch *string `json:"exactMatch,omitempty"`
RegexMatch *string `json:"regexMatch,omitempty"`
PrefixMatch *string `json:"prefixMatch,omitempty"`
SuffixMatch *string `json:"suffixMatch,omitempty"`
RangeMatch *Int64Range `json:"rangeMatch,omitempty"`
PresentMatch *bool `json:"presentMatch,omitempty"`
}
// Int64Range is a range for header range match.
type Int64Range struct {
Start int64 `json:"start"`
End int64 `json:"end"`
}
// ServiceUpdate contains information received from LDS and RDS responses,
// which is of interest to the registered service watcher.
type ServiceUpdate struct {
// Routes contain matchers+actions to route RPCs.
Routes []*Route
}
// ClusterUpdate contains information from a received CDS response, which is of
// interest to the registered CDS watcher.
type ClusterUpdate struct {
// ServiceName is the service name corresponding to the clusterName which
// is being watched for through CDS.
ServiceName string
// EnableLRS indicates whether or not load should be reported through LRS.
EnableLRS bool
}
// OverloadDropConfig contains the config to drop overloads.
type OverloadDropConfig struct {
Category string
Numerator uint32
Denominator uint32
}
// EndpointHealthStatus represents the health status of an endpoint.
type EndpointHealthStatus int32
const (
// EndpointHealthStatusUnknown represents HealthStatus UNKNOWN.
EndpointHealthStatusUnknown EndpointHealthStatus = iota
// EndpointHealthStatusHealthy represents HealthStatus HEALTHY.
EndpointHealthStatusHealthy
// EndpointHealthStatusUnhealthy represents HealthStatus UNHEALTHY.
EndpointHealthStatusUnhealthy
// EndpointHealthStatusDraining represents HealthStatus DRAINING.
EndpointHealthStatusDraining
// EndpointHealthStatusTimeout represents HealthStatus TIMEOUT.
EndpointHealthStatusTimeout
// EndpointHealthStatusDegraded represents HealthStatus DEGRADED.
EndpointHealthStatusDegraded
)
// Endpoint contains information of an endpoint.
type Endpoint struct {
Address string
HealthStatus EndpointHealthStatus
Weight uint32
}
// Locality contains information of a locality.
type Locality struct {
Endpoints []Endpoint
ID internal.LocalityID
Priority uint32
Weight uint32
}
// EndpointsUpdate contains an EDS update.
type EndpointsUpdate struct {
Drops []OverloadDropConfig
Localities []Locality
}
// Options provides all parameters required for the creation of an xDS client. // Options provides all parameters required for the creation of an xDS client.
type Options struct { type Options struct {
// Config contains a fully populated bootstrap config. It is the // Config contains a fully populated bootstrap config. It is the
@ -49,16 +236,13 @@ type Options struct {
TargetName string TargetName string
} }
// Interface to be overridden in tests.
type xdsv2Client interface {
addWatch(resourceType, resourceName string)
removeWatch(resourceType, resourceName string)
close()
}
// Function to be overridden in tests. // Function to be overridden in tests.
var newXDSV2Client = func(parent *Client, cc *grpc.ClientConn, nodeProto *corepb.Node, backoff func(int) time.Duration, logger *grpclog.PrefixLogger) xdsv2Client { var newAPIClient = func(apiVersion version.TransportAPI, cc *grpc.ClientConn, opts BuildOptions) (APIClient, error) {
return newV2Client(parent, cc, nodeProto, backoff, logger) cb := getAPIClientBuilder(apiVersion)
if cb == nil {
return nil, fmt.Errorf("no client builder for xDS API version: %v", apiVersion)
}
return cb.Build(cc, opts)
} }
// Client is a full fledged gRPC client which queries a set of discovery APIs // Client is a full fledged gRPC client which queries a set of discovery APIs
@ -68,20 +252,25 @@ var newXDSV2Client = func(parent *Client, cc *grpc.ClientConn, nodeProto *corepb
// A single client object will be shared by the xds resolver and balancer // A single client object will be shared by the xds resolver and balancer
// implementations. But the same client can only be shared by the same parent // implementations. But the same client can only be shared by the same parent
// ClientConn. // ClientConn.
//
// Implements UpdateHandler interface.
// TODO(easwars): Make a wrapper struct which implements this interface in the
// style of ccBalancerWrapper so that the Client type does not implement these
// exported methods.
type Client struct { type Client struct {
done *grpcsync.Event done *grpcsync.Event
opts Options opts Options
cc *grpc.ClientConn // Connection to the xDS server cc *grpc.ClientConn // Connection to the xDS server
v2c xdsv2Client // Actual xDS client implementation using the v2 API apiClient APIClient
logger *grpclog.PrefixLogger logger *grpclog.PrefixLogger
updateCh *buffer.Unbounded // chan *watcherInfoWithUpdate updateCh *buffer.Unbounded // chan *watcherInfoWithUpdate
mu sync.Mutex mu sync.Mutex
ldsWatchers map[string]map[*watchInfo]bool ldsWatchers map[string]map[*watchInfo]bool
ldsCache map[string]ldsUpdate ldsCache map[string]ListenerUpdate
rdsWatchers map[string]map[*watchInfo]bool rdsWatchers map[string]map[*watchInfo]bool
rdsCache map[string]rdsUpdate rdsCache map[string]RouteConfigUpdate
cdsWatchers map[string]map[*watchInfo]bool cdsWatchers map[string]map[*watchInfo]bool
cdsCache map[string]ClusterUpdate cdsCache map[string]ClusterUpdate
edsWatchers map[string]map[*watchInfo]bool edsWatchers map[string]map[*watchInfo]bool
@ -99,6 +288,17 @@ func New(opts Options) (*Client, error) {
return nil, errors.New("xds: no node_proto provided in options") return nil, errors.New("xds: no node_proto provided in options")
} }
switch opts.Config.TransportAPI {
case version.TransportV2:
if _, ok := opts.Config.NodeProto.(*v2corepb.Node); !ok {
return nil, fmt.Errorf("xds: Node proto type (%T) does not match API version: %v", opts.Config.NodeProto, opts.Config.TransportAPI)
}
case version.TransportV3:
if _, ok := opts.Config.NodeProto.(*v3corepb.Node); !ok {
return nil, fmt.Errorf("xds: Node proto type (%T) does not match API version: %v", opts.Config.NodeProto, opts.Config.TransportAPI)
}
}
dopts := []grpc.DialOption{ dopts := []grpc.DialOption{
opts.Config.Creds, opts.Config.Creds,
grpc.WithKeepaliveParams(keepalive.ClientParameters{ grpc.WithKeepaliveParams(keepalive.ClientParameters{
@ -114,9 +314,9 @@ func New(opts Options) (*Client, error) {
updateCh: buffer.NewUnbounded(), updateCh: buffer.NewUnbounded(),
ldsWatchers: make(map[string]map[*watchInfo]bool), ldsWatchers: make(map[string]map[*watchInfo]bool),
ldsCache: make(map[string]ldsUpdate), ldsCache: make(map[string]ListenerUpdate),
rdsWatchers: make(map[string]map[*watchInfo]bool), rdsWatchers: make(map[string]map[*watchInfo]bool),
rdsCache: make(map[string]rdsUpdate), rdsCache: make(map[string]RouteConfigUpdate),
cdsWatchers: make(map[string]map[*watchInfo]bool), cdsWatchers: make(map[string]map[*watchInfo]bool),
cdsCache: make(map[string]ClusterUpdate), cdsCache: make(map[string]ClusterUpdate),
edsWatchers: make(map[string]map[*watchInfo]bool), edsWatchers: make(map[string]map[*watchInfo]bool),
@ -132,13 +332,16 @@ func New(opts Options) (*Client, error) {
c.logger = prefixLogger((c)) c.logger = prefixLogger((c))
c.logger.Infof("Created ClientConn to xDS server: %s", opts.Config.BalancerName) c.logger.Infof("Created ClientConn to xDS server: %s", opts.Config.BalancerName)
if opts.Config.TransportAPI == version.TransportV2 { apiClient, err := newAPIClient(opts.Config.TransportAPI, cc, BuildOptions{
c.v2c = newXDSV2Client(c, cc, opts.Config.NodeProto.(*corepb.Node), backoff.DefaultExponential.Backoff, c.logger) Parent: c,
} else { NodeProto: opts.Config.NodeProto,
// TODO(easwars): Remove this once v3Client is ready. Backoff: backoff.DefaultExponential.Backoff,
return nil, errors.New("xds v3 client is not yet supported") Logger: c.logger,
})
if err != nil {
return nil, err
} }
c.apiClient = apiClient
c.logger.Infof("Created") c.logger.Infof("Created")
go c.run() go c.run()
return c, nil return c, nil
@ -173,7 +376,7 @@ func (c *Client) Close() {
c.done.Fire() c.done.Fire()
// TODO: Should we invoke the registered callbacks here with an error that // TODO: Should we invoke the registered callbacks here with an error that
// the client is closed? // the client is closed?
c.v2c.close() c.apiClient.Close()
c.cc.Close() c.cc.Close()
c.logger.Infof("Shutdown") c.logger.Infof("Shutdown")
} }

View File

@ -18,6 +18,8 @@
package client package client
import "google.golang.org/grpc/xds/internal/version"
type watcherInfoWithUpdate struct { type watcherInfoWithUpdate struct {
wi *watchInfo wi *watchInfo
update interface{} update interface{}
@ -45,19 +47,19 @@ func (c *Client) callCallback(wiu *watcherInfoWithUpdate) {
// canceled, and the user needs to take care of it. // canceled, and the user needs to take care of it.
var ccb func() var ccb func()
switch wiu.wi.typeURL { switch wiu.wi.typeURL {
case ldsURL: case version.V2ListenerURL:
if s, ok := c.ldsWatchers[wiu.wi.target]; ok && s[wiu.wi] { if s, ok := c.ldsWatchers[wiu.wi.target]; ok && s[wiu.wi] {
ccb = func() { wiu.wi.ldsCallback(wiu.update.(ldsUpdate), wiu.err) } ccb = func() { wiu.wi.ldsCallback(wiu.update.(ListenerUpdate), wiu.err) }
} }
case rdsURL: case version.V2RouteConfigURL:
if s, ok := c.rdsWatchers[wiu.wi.target]; ok && s[wiu.wi] { if s, ok := c.rdsWatchers[wiu.wi.target]; ok && s[wiu.wi] {
ccb = func() { wiu.wi.rdsCallback(wiu.update.(rdsUpdate), wiu.err) } ccb = func() { wiu.wi.rdsCallback(wiu.update.(RouteConfigUpdate), wiu.err) }
} }
case cdsURL: case version.V2ClusterURL:
if s, ok := c.cdsWatchers[wiu.wi.target]; ok && s[wiu.wi] { if s, ok := c.cdsWatchers[wiu.wi.target]; ok && s[wiu.wi] {
ccb = func() { wiu.wi.cdsCallback(wiu.update.(ClusterUpdate), wiu.err) } ccb = func() { wiu.wi.cdsCallback(wiu.update.(ClusterUpdate), wiu.err) }
} }
case edsURL: case version.V2EndpointsURL:
if s, ok := c.edsWatchers[wiu.wi.target]; ok && s[wiu.wi] { if s, ok := c.edsWatchers[wiu.wi.target]; ok && s[wiu.wi] {
ccb = func() { wiu.wi.edsCallback(wiu.update.(EndpointsUpdate), wiu.err) } ccb = func() { wiu.wi.edsCallback(wiu.update.(EndpointsUpdate), wiu.err) }
} }
@ -69,12 +71,12 @@ func (c *Client) callCallback(wiu *watcherInfoWithUpdate) {
} }
} }
// newLDSUpdate is called by the underlying xdsv2Client when it receives an xDS // NewListeners is called by the underlying xdsAPIClient when it receives an
// response. // xDS response.
// //
// A response can contain multiple resources. They will be parsed and put in a // A response can contain multiple resources. They will be parsed and put in a
// map from resource name to the resource content. // map from resource name to the resource content.
func (c *Client) newLDSUpdate(updates map[string]ldsUpdate) { func (c *Client) NewListeners(updates map[string]ListenerUpdate) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -104,12 +106,12 @@ func (c *Client) newLDSUpdate(updates map[string]ldsUpdate) {
// last watch is canceled. // last watch is canceled.
} }
// newRDSUpdate is called by the underlying xdsv2Client when it receives an xDS // NewRouteConfigs is called by the underlying xdsAPIClient when it receives an
// response. // xDS response.
// //
// A response can contain multiple resources. They will be parsed and put in a // A response can contain multiple resources. They will be parsed and put in a
// map from resource name to the resource content. // map from resource name to the resource content.
func (c *Client) newRDSUpdate(updates map[string]rdsUpdate) { func (c *Client) NewRouteConfigs(updates map[string]RouteConfigUpdate) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -125,12 +127,12 @@ func (c *Client) newRDSUpdate(updates map[string]rdsUpdate) {
} }
} }
// newCDSUpdate is called by the underlying xdsv2Client when it receives an xDS // NewClusters is called by the underlying xdsAPIClient when it receives an xDS
// response. // response.
// //
// A response can contain multiple resources. They will be parsed and put in a // A response can contain multiple resources. They will be parsed and put in a
// map from resource name to the resource content. // map from resource name to the resource content.
func (c *Client) newCDSUpdate(updates map[string]ClusterUpdate) { func (c *Client) NewClusters(updates map[string]ClusterUpdate) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -160,12 +162,12 @@ func (c *Client) newCDSUpdate(updates map[string]ClusterUpdate) {
// last watch is canceled. // last watch is canceled.
} }
// newEDSUpdate is called by the underlying xdsv2Client when it receives an xDS // NewEndpoints is called by the underlying xdsAPIClient when it receives an
// response. // xDS response.
// //
// A response can contain multiple resources. They will be parsed and put in a // A response can contain multiple resources. They will be parsed and put in a
// map from resource name to the resource content. // map from resource name to the resource content.
func (c *Client) newEDSUpdate(updates map[string]EndpointsUpdate) { func (c *Client) NewEndpoints(updates map[string]EndpointsUpdate) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()

View File

@ -0,0 +1,312 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package client
import (
"testing"
v2xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
v2corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
"github.com/golang/protobuf/proto"
anypb "github.com/golang/protobuf/ptypes/any"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/grpc/xds/internal/version"
)
func (s) TestValidateCluster(t *testing.T) {
const (
clusterName = "clusterName"
serviceName = "service"
)
var (
emptyUpdate = ClusterUpdate{ServiceName: "", EnableLRS: false}
)
tests := []struct {
name string
cluster *v3clusterpb.Cluster
wantUpdate ClusterUpdate
wantErr bool
}{
{
name: "non-eds-cluster-type",
cluster: &v3clusterpb.Cluster{
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_STATIC},
EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
EdsConfig: &v3corepb.ConfigSource{
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
Ads: &v3corepb.AggregatedConfigSource{},
},
},
},
LbPolicy: v3clusterpb.Cluster_LEAST_REQUEST,
},
wantUpdate: emptyUpdate,
wantErr: true,
},
{
name: "no-eds-config",
cluster: &v3clusterpb.Cluster{
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
},
wantUpdate: emptyUpdate,
wantErr: true,
},
{
name: "no-ads-config-source",
cluster: &v3clusterpb.Cluster{
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{},
LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
},
wantUpdate: emptyUpdate,
wantErr: true,
},
{
name: "non-round-robin-lb-policy",
cluster: &v3clusterpb.Cluster{
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
EdsConfig: &v3corepb.ConfigSource{
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
Ads: &v3corepb.AggregatedConfigSource{},
},
},
},
LbPolicy: v3clusterpb.Cluster_LEAST_REQUEST,
},
wantUpdate: emptyUpdate,
wantErr: true,
},
{
name: "happy-case-no-service-name-no-lrs",
cluster: &v3clusterpb.Cluster{
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
EdsConfig: &v3corepb.ConfigSource{
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
Ads: &v3corepb.AggregatedConfigSource{},
},
},
},
LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
},
wantUpdate: emptyUpdate,
},
{
name: "happy-case-no-lrs",
cluster: &v3clusterpb.Cluster{
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
EdsConfig: &v3corepb.ConfigSource{
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
Ads: &v3corepb.AggregatedConfigSource{},
},
},
ServiceName: serviceName,
},
LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
},
wantUpdate: ClusterUpdate{ServiceName: serviceName, EnableLRS: false},
},
{
name: "happiest-case",
cluster: &v3clusterpb.Cluster{
Name: clusterName,
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
EdsConfig: &v3corepb.ConfigSource{
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
Ads: &v3corepb.AggregatedConfigSource{},
},
},
ServiceName: serviceName,
},
LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
LrsServer: &v3corepb.ConfigSource{
ConfigSourceSpecifier: &v3corepb.ConfigSource_Self{
Self: &v3corepb.SelfConfigSource{},
},
},
},
wantUpdate: ClusterUpdate{ServiceName: serviceName, EnableLRS: true},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
update, err := validateCluster(test.cluster)
if ((err != nil) != test.wantErr) || !cmp.Equal(update, test.wantUpdate, cmpopts.EquateEmpty()) {
t.Errorf("validateCluster(%+v) = (%v, %v), wantErr: (%v, %v)", test.cluster, update, err, test.wantUpdate, test.wantErr)
}
})
}
}
func (s) TestUnmarshalCluster(t *testing.T) {
const (
v2ClusterName = "v2clusterName"
v3ClusterName = "v3clusterName"
v2Service = "v2Service"
v3Service = "v2Service"
)
var (
v2Cluster = &v2xdspb.Cluster{
Name: v2ClusterName,
ClusterDiscoveryType: &v2xdspb.Cluster_Type{Type: v2xdspb.Cluster_EDS},
EdsClusterConfig: &v2xdspb.Cluster_EdsClusterConfig{
EdsConfig: &v2corepb.ConfigSource{
ConfigSourceSpecifier: &v2corepb.ConfigSource_Ads{
Ads: &v2corepb.AggregatedConfigSource{},
},
},
ServiceName: v2Service,
},
LbPolicy: v2xdspb.Cluster_ROUND_ROBIN,
LrsServer: &v2corepb.ConfigSource{
ConfigSourceSpecifier: &v2corepb.ConfigSource_Self{
Self: &v2corepb.SelfConfigSource{},
},
},
}
v3Cluster = &v3clusterpb.Cluster{
Name: v3ClusterName,
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
EdsConfig: &v3corepb.ConfigSource{
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
Ads: &v3corepb.AggregatedConfigSource{},
},
},
ServiceName: v3Service,
},
LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
LrsServer: &v3corepb.ConfigSource{
ConfigSourceSpecifier: &v3corepb.ConfigSource_Self{
Self: &v3corepb.SelfConfigSource{},
},
},
}
)
tests := []struct {
name string
resources []*anypb.Any
wantUpdate map[string]ClusterUpdate
wantErr bool
}{
{
name: "non-cluster resource type",
resources: []*anypb.Any{{TypeUrl: version.V3HTTPConnManagerURL}},
wantErr: true,
},
{
name: "badly marshaled cluster resource",
resources: []*anypb.Any{
{
TypeUrl: version.V3ClusterURL,
Value: []byte{1, 2, 3, 4},
},
},
wantErr: true,
},
{
name: "bad cluster resource",
resources: []*anypb.Any{
{
TypeUrl: version.V3ClusterURL,
Value: func() []byte {
cl := &v3clusterpb.Cluster{
ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_STATIC},
}
mcl, _ := proto.Marshal(cl)
return mcl
}(),
},
},
wantErr: true,
},
{
name: "v2 cluster",
resources: []*anypb.Any{
{
TypeUrl: version.V3ClusterURL,
Value: func() []byte {
mcl, _ := proto.Marshal(v2Cluster)
return mcl
}(),
},
},
wantUpdate: map[string]ClusterUpdate{
v2ClusterName: {ServiceName: v2Service, EnableLRS: true},
},
},
{
name: "v3 cluster",
resources: []*anypb.Any{
{
TypeUrl: version.V3ClusterURL,
Value: func() []byte {
mcl, _ := proto.Marshal(v3Cluster)
return mcl
}(),
},
},
wantUpdate: map[string]ClusterUpdate{
v3ClusterName: {ServiceName: v3Service, EnableLRS: true},
},
},
{
name: "multiple clusters",
resources: []*anypb.Any{
{
TypeUrl: version.V3ClusterURL,
Value: func() []byte {
mcl, _ := proto.Marshal(v2Cluster)
return mcl
}(),
},
{
TypeUrl: version.V3ClusterURL,
Value: func() []byte {
mcl, _ := proto.Marshal(v3Cluster)
return mcl
}(),
},
},
wantUpdate: map[string]ClusterUpdate{
v2ClusterName: {ServiceName: v2Service, EnableLRS: true},
v3ClusterName: {ServiceName: v3Service, EnableLRS: true},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
update, err := UnmarshalCluster(test.resources, nil)
if ((err != nil) != test.wantErr) || !cmp.Equal(update, test.wantUpdate, cmpopts.EquateEmpty()) {
t.Errorf("UnmarshalCluster(%v) = (%+v, %v) want (%+v, %v)", test.resources, update, err, test.wantUpdate, test.wantErr)
}
})
}
}

View File

@ -0,0 +1,312 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package client
import (
"fmt"
"net"
"strconv"
"testing"
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"github.com/golang/protobuf/proto"
anypb "github.com/golang/protobuf/ptypes/any"
wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/grpc/xds/internal"
"google.golang.org/grpc/xds/internal/version"
)
func (s) TestEDSParseRespProto(t *testing.T) {
tests := []struct {
name string
m *v3endpointpb.ClusterLoadAssignment
want EndpointsUpdate
wantErr bool
}{
{
name: "missing-priority",
m: func() *v3endpointpb.ClusterLoadAssignment {
clab0 := newClaBuilder("test", nil)
clab0.addLocality("locality-1", 1, 0, []string{"addr1:314"}, nil)
clab0.addLocality("locality-2", 1, 2, []string{"addr2:159"}, nil)
return clab0.Build()
}(),
want: EndpointsUpdate{},
wantErr: true,
},
{
name: "missing-locality-ID",
m: func() *v3endpointpb.ClusterLoadAssignment {
clab0 := newClaBuilder("test", nil)
clab0.addLocality("", 1, 0, []string{"addr1:314"}, nil)
return clab0.Build()
}(),
want: EndpointsUpdate{},
wantErr: true,
},
{
name: "good",
m: func() *v3endpointpb.ClusterLoadAssignment {
clab0 := newClaBuilder("test", nil)
clab0.addLocality("locality-1", 1, 1, []string{"addr1:314"}, &addLocalityOptions{
Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_UNHEALTHY},
Weight: []uint32{271},
})
clab0.addLocality("locality-2", 1, 0, []string{"addr2:159"}, &addLocalityOptions{
Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_DRAINING},
Weight: []uint32{828},
})
return clab0.Build()
}(),
want: EndpointsUpdate{
Drops: nil,
Localities: []Locality{
{
Endpoints: []Endpoint{{
Address: "addr1:314",
HealthStatus: EndpointHealthStatusUnhealthy,
Weight: 271,
}},
ID: internal.LocalityID{SubZone: "locality-1"},
Priority: 1,
Weight: 1,
},
{
Endpoints: []Endpoint{{
Address: "addr2:159",
HealthStatus: EndpointHealthStatusDraining,
Weight: 828,
}},
ID: internal.LocalityID{SubZone: "locality-2"},
Priority: 0,
Weight: 1,
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseEDSRespProto(tt.m)
if (err != nil) != tt.wantErr {
t.Errorf("parseEDSRespProto() error = %v, wantErr %v", err, tt.wantErr)
return
}
if d := cmp.Diff(got, tt.want); d != "" {
t.Errorf("parseEDSRespProto() got = %v, want %v, diff: %v", got, tt.want, d)
}
})
}
}
func (s) TestUnmarshalEndpoints(t *testing.T) {
tests := []struct {
name string
resources []*anypb.Any
wantUpdate map[string]EndpointsUpdate
wantErr bool
}{
{
name: "non-clusterLoadAssignment resource type",
resources: []*anypb.Any{{TypeUrl: version.V3HTTPConnManagerURL}},
wantErr: true,
},
{
name: "badly marshaled clusterLoadAssignment resource",
resources: []*anypb.Any{
{
TypeUrl: version.V3EndpointsURL,
Value: []byte{1, 2, 3, 4},
},
},
wantErr: true,
},
{
name: "bad endpoints resource",
resources: []*anypb.Any{
{
TypeUrl: version.V3EndpointsURL,
Value: func() []byte {
clab0 := newClaBuilder("test", nil)
clab0.addLocality("locality-1", 1, 0, []string{"addr1:314"}, nil)
clab0.addLocality("locality-2", 1, 2, []string{"addr2:159"}, nil)
e := clab0.Build()
me, _ := proto.Marshal(e)
return me
}(),
},
},
wantErr: true,
},
{
name: "v3 endpoints",
resources: []*anypb.Any{
{
TypeUrl: version.V3EndpointsURL,
Value: func() []byte {
clab0 := newClaBuilder("test", nil)
clab0.addLocality("locality-1", 1, 1, []string{"addr1:314"}, &addLocalityOptions{
Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_UNHEALTHY},
Weight: []uint32{271},
})
clab0.addLocality("locality-2", 1, 0, []string{"addr2:159"}, &addLocalityOptions{
Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_DRAINING},
Weight: []uint32{828},
})
e := clab0.Build()
me, _ := proto.Marshal(e)
return me
}(),
},
},
wantUpdate: map[string]EndpointsUpdate{
"test": {
Drops: nil,
Localities: []Locality{
{
Endpoints: []Endpoint{{
Address: "addr1:314",
HealthStatus: EndpointHealthStatusUnhealthy,
Weight: 271,
}},
ID: internal.LocalityID{SubZone: "locality-1"},
Priority: 1,
Weight: 1,
},
{
Endpoints: []Endpoint{{
Address: "addr2:159",
HealthStatus: EndpointHealthStatusDraining,
Weight: 828,
}},
ID: internal.LocalityID{SubZone: "locality-2"},
Priority: 0,
Weight: 1,
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
update, err := UnmarshalEndpoints(test.resources, nil)
if ((err != nil) != test.wantErr) || !cmp.Equal(update, test.wantUpdate, cmpopts.EquateEmpty()) {
t.Errorf("UnmarshalEndpoints(%v) = (%+v, %v) want (%+v, %v)", test.resources, update, err, test.wantUpdate, test.wantErr)
}
})
}
}
// claBuilder builds a ClusterLoadAssignment, aka EDS
// response.
type claBuilder struct {
v *v3endpointpb.ClusterLoadAssignment
}
// newClaBuilder creates a claBuilder.
func newClaBuilder(clusterName string, dropPercents []uint32) *claBuilder {
var drops []*v3endpointpb.ClusterLoadAssignment_Policy_DropOverload
for i, d := range dropPercents {
drops = append(drops, &v3endpointpb.ClusterLoadAssignment_Policy_DropOverload{
Category: fmt.Sprintf("test-drop-%d", i),
DropPercentage: &v3typepb.FractionalPercent{
Numerator: d,
Denominator: v3typepb.FractionalPercent_HUNDRED,
},
})
}
return &claBuilder{
v: &v3endpointpb.ClusterLoadAssignment{
ClusterName: clusterName,
Policy: &v3endpointpb.ClusterLoadAssignment_Policy{
DropOverloads: drops,
},
},
}
}
// addLocalityOptions contains options when adding locality to the builder.
type addLocalityOptions struct {
Health []v3corepb.HealthStatus
Weight []uint32
}
// addLocality adds a locality to the builder.
func (clab *claBuilder) addLocality(subzone string, weight uint32, priority uint32, addrsWithPort []string, opts *addLocalityOptions) {
var lbEndPoints []*v3endpointpb.LbEndpoint
for i, a := range addrsWithPort {
host, portStr, err := net.SplitHostPort(a)
if err != nil {
panic("failed to split " + a)
}
port, err := strconv.Atoi(portStr)
if err != nil {
panic("failed to atoi " + portStr)
}
lbe := &v3endpointpb.LbEndpoint{
HostIdentifier: &v3endpointpb.LbEndpoint_Endpoint{
Endpoint: &v3endpointpb.Endpoint{
Address: &v3corepb.Address{
Address: &v3corepb.Address_SocketAddress{
SocketAddress: &v3corepb.SocketAddress{
Protocol: v3corepb.SocketAddress_TCP,
Address: host,
PortSpecifier: &v3corepb.SocketAddress_PortValue{
PortValue: uint32(port)}}}}}},
}
if opts != nil {
if i < len(opts.Health) {
lbe.HealthStatus = opts.Health[i]
}
if i < len(opts.Weight) {
lbe.LoadBalancingWeight = &wrapperspb.UInt32Value{Value: opts.Weight[i]}
}
}
lbEndPoints = append(lbEndPoints, lbe)
}
var localityID *v3corepb.Locality
if subzone != "" {
localityID = &v3corepb.Locality{
Region: "",
Zone: "",
SubZone: subzone,
}
}
clab.v.Endpoints = append(clab.v.Endpoints, &v3endpointpb.LocalityLbEndpoints{
Locality: localityID,
LbEndpoints: lbEndPoints,
LoadBalancingWeight: &wrapperspb.UInt32Value{Value: weight},
Priority: priority,
})
}
// Build builds ClusterLoadAssignment.
func (clab *claBuilder) Build() *v3endpointpb.ClusterLoadAssignment {
return clab.v
}

View File

@ -0,0 +1,171 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package client
import (
"testing"
v2xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
v2corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
v2httppb "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2"
v2listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v2"
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
"github.com/golang/protobuf/proto"
anypb "github.com/golang/protobuf/ptypes/any"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/grpc/xds/internal/version"
)
func (s) TestUnmarshalListener(t *testing.T) {
const (
v2LDSTarget = "lds.target.good:2222"
v3LDSTarget = "lds.target.good:3333"
v2RouteConfigName = "v2RouteConfig"
v3RouteConfigName = "v3RouteConfig"
)
var (
v2Lis = &anypb.Any{
TypeUrl: version.V2ListenerURL,
Value: func() []byte {
cm := &v2httppb.HttpConnectionManager{
RouteSpecifier: &v2httppb.HttpConnectionManager_Rds{
Rds: &v2httppb.Rds{
ConfigSource: &v2corepb.ConfigSource{
ConfigSourceSpecifier: &v2corepb.ConfigSource_Ads{Ads: &v2corepb.AggregatedConfigSource{}},
},
RouteConfigName: v2RouteConfigName,
},
},
}
mcm, _ := proto.Marshal(cm)
lis := &v2xdspb.Listener{
Name: v2LDSTarget,
ApiListener: &v2listenerpb.ApiListener{
ApiListener: &anypb.Any{
TypeUrl: version.V2HTTPConnManagerURL,
Value: mcm,
},
},
}
mLis, _ := proto.Marshal(lis)
return mLis
}(),
}
v3Lis = &anypb.Any{
TypeUrl: version.V3ListenerURL,
Value: func() []byte {
cm := &v3httppb.HttpConnectionManager{
RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
Rds: &v3httppb.Rds{
ConfigSource: &v3corepb.ConfigSource{
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
},
RouteConfigName: v3RouteConfigName,
},
},
}
mcm, _ := proto.Marshal(cm)
lis := &v3listenerpb.Listener{
Name: v3LDSTarget,
ApiListener: &v3listenerpb.ApiListener{
ApiListener: &anypb.Any{
TypeUrl: version.V3HTTPConnManagerURL,
Value: mcm,
},
},
}
mLis, _ := proto.Marshal(lis)
return mLis
}(),
}
)
tests := []struct {
name string
resources []*anypb.Any
wantUpdate map[string]ListenerUpdate
wantErr bool
}{
{
name: "non-listener resource",
resources: []*anypb.Any{{TypeUrl: version.V3HTTPConnManagerURL}},
wantErr: true,
},
{
name: "badly marshaled listener resource",
resources: []*anypb.Any{
{
TypeUrl: version.V3ListenerURL,
Value: func() []byte {
lis := &v3listenerpb.Listener{
Name: v3LDSTarget,
ApiListener: &v3listenerpb.ApiListener{
ApiListener: &anypb.Any{
TypeUrl: version.V3HTTPConnManagerURL,
Value: []byte{1, 2, 3, 4},
},
},
}
mLis, _ := proto.Marshal(lis)
return mLis
}(),
},
},
wantErr: true,
},
{
name: "empty resource list",
},
{
name: "v2 listener resource",
resources: []*anypb.Any{v2Lis},
wantUpdate: map[string]ListenerUpdate{
v2LDSTarget: {RouteConfigName: v2RouteConfigName},
},
},
{
name: "v3 listener resource",
resources: []*anypb.Any{v3Lis},
wantUpdate: map[string]ListenerUpdate{
v3LDSTarget: {RouteConfigName: v3RouteConfigName},
},
},
{
name: "multiple listener resources",
resources: []*anypb.Any{v2Lis, v3Lis},
wantUpdate: map[string]ListenerUpdate{
v2LDSTarget: {RouteConfigName: v2RouteConfigName},
v3LDSTarget: {RouteConfigName: v3RouteConfigName},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
update, err := UnmarshalListener(test.resources, nil)
if ((err != nil) != test.wantErr) || !cmp.Equal(update, test.wantUpdate, cmpopts.EquateEmpty()) {
t.Errorf("UnmarshalListener(%v) = (%v, %v) want (%v, %v)", test.resources, update, err, test.wantUpdate, test.wantErr)
}
})
}
}

View File

@ -0,0 +1,854 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package client
import (
"testing"
v2xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
v2routepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"github.com/golang/protobuf/proto"
anypb "github.com/golang/protobuf/ptypes/any"
wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/grpc/xds/internal/version"
)
func (s) TestGetRouteConfigFromListener(t *testing.T) {
const (
goodLDSTarget = "lds.target.good:1111"
goodRouteConfigName = "GoodRouteConfig"
)
tests := []struct {
name string
lis *v3listenerpb.Listener
wantRoute string
wantErr bool
}{
{
name: "no-apiListener-field",
lis: &v3listenerpb.Listener{},
wantRoute: "",
wantErr: true,
},
{
name: "badly-marshaled-apiListener",
lis: &v3listenerpb.Listener{
Name: goodLDSTarget,
ApiListener: &v3listenerpb.ApiListener{
ApiListener: &anypb.Any{
TypeUrl: version.V3HTTPConnManagerURL,
Value: []byte{1, 2, 3, 4},
},
},
},
wantRoute: "",
wantErr: true,
},
{
name: "wrong-type-in-apiListener",
lis: &v3listenerpb.Listener{
Name: goodLDSTarget,
ApiListener: &v3listenerpb.ApiListener{
ApiListener: &anypb.Any{
TypeUrl: version.V2ListenerURL,
Value: func() []byte {
cm := &v3httppb.HttpConnectionManager{
RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
Rds: &v3httppb.Rds{
ConfigSource: &v3corepb.ConfigSource{
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
},
RouteConfigName: goodRouteConfigName}}}
mcm, _ := proto.Marshal(cm)
return mcm
}()}}},
wantRoute: "",
wantErr: true,
},
{
name: "empty-httpConnMgr-in-apiListener",
lis: &v3listenerpb.Listener{
Name: goodLDSTarget,
ApiListener: &v3listenerpb.ApiListener{
ApiListener: &anypb.Any{
TypeUrl: version.V3HTTPConnManagerURL,
Value: func() []byte {
cm := &v3httppb.HttpConnectionManager{
RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
Rds: &v3httppb.Rds{},
},
}
mcm, _ := proto.Marshal(cm)
return mcm
}()}}},
wantRoute: "",
wantErr: true,
},
{
name: "scopedRoutes-routeConfig-in-apiListener",
lis: &v3listenerpb.Listener{
Name: goodLDSTarget,
ApiListener: &v3listenerpb.ApiListener{
ApiListener: &anypb.Any{
TypeUrl: version.V3HTTPConnManagerURL,
Value: func() []byte {
cm := &v3httppb.HttpConnectionManager{
RouteSpecifier: &v3httppb.HttpConnectionManager_ScopedRoutes{},
}
mcm, _ := proto.Marshal(cm)
return mcm
}()}}},
wantRoute: "",
wantErr: true,
},
{
name: "rds.ConfigSource-in-apiListener-is-not-ADS",
lis: &v3listenerpb.Listener{
Name: goodLDSTarget,
ApiListener: &v3listenerpb.ApiListener{
ApiListener: &anypb.Any{
TypeUrl: version.V3HTTPConnManagerURL,
Value: func() []byte {
cm := &v3httppb.HttpConnectionManager{
RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
Rds: &v3httppb.Rds{
ConfigSource: &v3corepb.ConfigSource{
ConfigSourceSpecifier: &v3corepb.ConfigSource_Path{
Path: "/some/path",
},
},
RouteConfigName: goodRouteConfigName}}}
mcm, _ := proto.Marshal(cm)
return mcm
}()}}},
wantRoute: "",
wantErr: true,
},
{
name: "goodListener",
lis: &v3listenerpb.Listener{
Name: goodLDSTarget,
ApiListener: &v3listenerpb.ApiListener{
ApiListener: &anypb.Any{
TypeUrl: version.V3HTTPConnManagerURL,
Value: func() []byte {
cm := &v3httppb.HttpConnectionManager{
RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
Rds: &v3httppb.Rds{
ConfigSource: &v3corepb.ConfigSource{
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
},
RouteConfigName: goodRouteConfigName}}}
mcm, _ := proto.Marshal(cm)
return mcm
}()}}},
wantRoute: goodRouteConfigName,
wantErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
gotRoute, err := getRouteConfigNameFromListener(test.lis, nil)
if (err != nil) != test.wantErr || gotRoute != test.wantRoute {
t.Errorf("getRouteConfigNameFromListener(%+v) = (%s, %v), want (%s, %v)", test.lis, gotRoute, err, test.wantRoute, test.wantErr)
}
})
}
}
func (s) TestRDSGenerateRDSUpdateFromRouteConfiguration(t *testing.T) {
const (
uninterestingDomain = "uninteresting.domain"
uninterestingClusterName = "uninterestingClusterName"
ldsTarget = "lds.target.good:1111"
routeName = "routeName"
clusterName = "clusterName"
)
tests := []struct {
name string
rc *v3routepb.RouteConfiguration
wantUpdate RouteConfigUpdate
wantError bool
}{
{
name: "no-virtual-hosts-in-rc",
rc: &v3routepb.RouteConfiguration{},
wantError: true,
},
{
name: "no-domains-in-rc",
rc: &v3routepb.RouteConfiguration{
VirtualHosts: []*v3routepb.VirtualHost{{}},
},
wantError: true,
},
{
name: "non-matching-domain-in-rc",
rc: &v3routepb.RouteConfiguration{
VirtualHosts: []*v3routepb.VirtualHost{
{Domains: []string{uninterestingDomain}},
},
},
wantError: true,
},
{
name: "no-routes-in-rc",
rc: &v3routepb.RouteConfiguration{
VirtualHosts: []*v3routepb.VirtualHost{
{Domains: []string{ldsTarget}},
},
},
wantError: true,
},
{
name: "default-route-match-field-is-nil",
rc: &v3routepb.RouteConfiguration{
VirtualHosts: []*v3routepb.VirtualHost{
{
Domains: []string{ldsTarget},
Routes: []*v3routepb.Route{
{
Action: &v3routepb.Route_Route{
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
},
},
},
},
},
},
},
wantError: true,
},
{
name: "default-route-match-field-is-non-nil",
rc: &v3routepb.RouteConfiguration{
VirtualHosts: []*v3routepb.VirtualHost{
{
Domains: []string{ldsTarget},
Routes: []*v3routepb.Route{
{
Match: &v3routepb.RouteMatch{},
Action: &v3routepb.Route_Route{},
},
},
},
},
},
wantError: true,
},
{
name: "default-route-routeaction-field-is-nil",
rc: &v3routepb.RouteConfiguration{
VirtualHosts: []*v3routepb.VirtualHost{
{
Domains: []string{ldsTarget},
Routes: []*v3routepb.Route{{}},
},
},
},
wantError: true,
},
{
name: "default-route-cluster-field-is-empty",
rc: &v3routepb.RouteConfiguration{
VirtualHosts: []*v3routepb.VirtualHost{
{
Domains: []string{ldsTarget},
Routes: []*v3routepb.Route{
{
Action: &v3routepb.Route_Route{
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_ClusterHeader{},
},
},
},
},
},
},
},
wantError: true,
},
{
// default route's match sets case-sensitive to false.
name: "good-route-config-but-with-casesensitive-false",
rc: &v3routepb.RouteConfiguration{
Name: routeName,
VirtualHosts: []*v3routepb.VirtualHost{{
Domains: []string{ldsTarget},
Routes: []*v3routepb.Route{{
Match: &v3routepb.RouteMatch{
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
CaseSensitive: &wrapperspb.BoolValue{Value: false},
},
Action: &v3routepb.Route_Route{
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
}}}}}}},
wantError: true,
},
{
name: "good-route-config-with-empty-string-route",
rc: &v3routepb.RouteConfiguration{
Name: routeName,
VirtualHosts: []*v3routepb.VirtualHost{
{
Domains: []string{uninterestingDomain},
Routes: []*v3routepb.Route{
{
Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
Action: &v3routepb.Route_Route{
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: uninterestingClusterName},
},
},
},
},
},
{
Domains: []string{ldsTarget},
Routes: []*v3routepb.Route{
{
Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
Action: &v3routepb.Route_Route{
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
},
},
},
},
},
},
},
wantUpdate: RouteConfigUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{clusterName: 1}}}},
},
{
// default route's match is not empty string, but "/".
name: "good-route-config-with-slash-string-route",
rc: &v3routepb.RouteConfiguration{
Name: routeName,
VirtualHosts: []*v3routepb.VirtualHost{
{
Domains: []string{ldsTarget},
Routes: []*v3routepb.Route{
{
Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
Action: &v3routepb.Route_Route{
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName},
},
},
},
},
},
},
},
wantUpdate: RouteConfigUpdate{Routes: []*Route{{Prefix: newStringP("/"), Action: map[string]uint32{clusterName: 1}}}},
},
{
// weights not add up to total-weight.
name: "route-config-with-weighted_clusters_weights_not_add_up",
rc: &v3routepb.RouteConfiguration{
Name: routeName,
VirtualHosts: []*v3routepb.VirtualHost{
{
Domains: []string{ldsTarget},
Routes: []*v3routepb.Route{
{
Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
Action: &v3routepb.Route_Route{
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
WeightedClusters: &v3routepb.WeightedCluster{
Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
{Name: "a", Weight: &wrapperspb.UInt32Value{Value: 2}},
{Name: "b", Weight: &wrapperspb.UInt32Value{Value: 3}},
{Name: "c", Weight: &wrapperspb.UInt32Value{Value: 5}},
},
TotalWeight: &wrapperspb.UInt32Value{Value: 30},
},
},
},
},
},
},
},
},
},
wantError: true,
},
{
name: "good-route-config-with-weighted_clusters",
rc: &v3routepb.RouteConfiguration{
Name: routeName,
VirtualHosts: []*v3routepb.VirtualHost{
{
Domains: []string{ldsTarget},
Routes: []*v3routepb.Route{
{
Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
Action: &v3routepb.Route_Route{
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
WeightedClusters: &v3routepb.WeightedCluster{
Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
{Name: "a", Weight: &wrapperspb.UInt32Value{Value: 2}},
{Name: "b", Weight: &wrapperspb.UInt32Value{Value: 3}},
{Name: "c", Weight: &wrapperspb.UInt32Value{Value: 5}},
},
TotalWeight: &wrapperspb.UInt32Value{Value: 10},
},
},
},
},
},
},
},
},
},
wantUpdate: RouteConfigUpdate{Routes: []*Route{{Prefix: newStringP("/"), Action: map[string]uint32{"a": 2, "b": 3, "c": 5}}}},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
gotUpdate, gotError := generateRDSUpdateFromRouteConfiguration(test.rc, ldsTarget, nil)
if (gotError != nil) != test.wantError || !cmp.Equal(gotUpdate, test.wantUpdate, cmpopts.EquateEmpty()) {
t.Errorf("generateRDSUpdateFromRouteConfiguration(%+v, %v) = %v, want %v", test.rc, ldsTarget, gotUpdate, test.wantUpdate)
}
})
}
}
func (s) TestUnmarshalRouteConfig(t *testing.T) {
const (
ldsTarget = "lds.target.good:1111"
uninterestingDomain = "uninteresting.domain"
uninterestingClusterName = "uninterestingClusterName"
v2RouteConfigName = "v2RouteConfig"
v3RouteConfigName = "v3RouteConfig"
v2ClusterName = "v2Cluster"
v3ClusterName = "v3Cluster"
)
var (
v2VirtualHost = []*v2routepb.VirtualHost{
{
Domains: []string{uninterestingDomain},
Routes: []*v2routepb.Route{
{
Match: &v2routepb.RouteMatch{PathSpecifier: &v2routepb.RouteMatch_Prefix{Prefix: ""}},
Action: &v2routepb.Route_Route{
Route: &v2routepb.RouteAction{
ClusterSpecifier: &v2routepb.RouteAction_Cluster{Cluster: uninterestingClusterName},
},
},
},
},
},
{
Domains: []string{ldsTarget},
Routes: []*v2routepb.Route{
{
Match: &v2routepb.RouteMatch{PathSpecifier: &v2routepb.RouteMatch_Prefix{Prefix: ""}},
Action: &v2routepb.Route_Route{
Route: &v2routepb.RouteAction{
ClusterSpecifier: &v2routepb.RouteAction_Cluster{Cluster: v2ClusterName},
},
},
},
},
},
}
v2RouteConfig = &anypb.Any{
TypeUrl: version.V2RouteConfigURL,
Value: func() []byte {
rc := &v2xdspb.RouteConfiguration{
Name: v2RouteConfigName,
VirtualHosts: v2VirtualHost,
}
m, _ := proto.Marshal(rc)
return m
}(),
}
v3VirtualHost = []*v3routepb.VirtualHost{
{
Domains: []string{uninterestingDomain},
Routes: []*v3routepb.Route{
{
Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
Action: &v3routepb.Route_Route{
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: uninterestingClusterName},
},
},
},
},
},
{
Domains: []string{ldsTarget},
Routes: []*v3routepb.Route{
{
Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
Action: &v3routepb.Route_Route{
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: v3ClusterName},
},
},
},
},
},
}
v3RouteConfig = &anypb.Any{
TypeUrl: version.V2RouteConfigURL,
Value: func() []byte {
rc := &v3routepb.RouteConfiguration{
Name: v3RouteConfigName,
VirtualHosts: v3VirtualHost,
}
m, _ := proto.Marshal(rc)
return m
}(),
}
)
tests := []struct {
name string
resources []*anypb.Any
wantUpdate map[string]RouteConfigUpdate
wantErr bool
}{
{
name: "non-routeConfig resource type",
resources: []*anypb.Any{{TypeUrl: version.V3HTTPConnManagerURL}},
wantErr: true,
},
{
name: "badly marshaled routeconfig resource",
resources: []*anypb.Any{
{
TypeUrl: version.V3RouteConfigURL,
Value: []byte{1, 2, 3, 4},
},
},
wantErr: true,
},
{
name: "bad routeConfig resource",
resources: []*anypb.Any{
{
TypeUrl: version.V3RouteConfigURL,
Value: func() []byte {
rc := &v3routepb.RouteConfiguration{
VirtualHosts: []*v3routepb.VirtualHost{
{Domains: []string{uninterestingDomain}},
},
}
m, _ := proto.Marshal(rc)
return m
}(),
},
},
wantErr: true,
},
{
name: "empty resource list",
},
{
name: "v2 routeConfig resource",
resources: []*anypb.Any{v2RouteConfig},
wantUpdate: map[string]RouteConfigUpdate{
v2RouteConfigName: {Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{v2ClusterName: 1}}}},
},
},
{
name: "v3 routeConfig resource",
resources: []*anypb.Any{v3RouteConfig},
wantUpdate: map[string]RouteConfigUpdate{
v3RouteConfigName: {Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{v3ClusterName: 1}}}},
},
},
{
name: "multiple routeConfig resources",
resources: []*anypb.Any{v2RouteConfig, v3RouteConfig},
wantUpdate: map[string]RouteConfigUpdate{
v3RouteConfigName: {Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{v3ClusterName: 1}}}},
v2RouteConfigName: {Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{v2ClusterName: 1}}}},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
update, err := UnmarshalRouteConfig(test.resources, ldsTarget, nil)
if ((err != nil) != test.wantErr) || !cmp.Equal(update, test.wantUpdate, cmpopts.EquateEmpty()) {
t.Errorf("UnmarshalRouteConfig(%v, %v) = (%v, %v) want (%v, %v)", test.resources, ldsTarget, update, err, test.wantUpdate, test.wantErr)
}
})
}
}
func (s) TestMatchTypeForDomain(t *testing.T) {
tests := []struct {
d string
want domainMatchType
}{
{d: "", want: domainMatchTypeInvalid},
{d: "*", want: domainMatchTypeUniversal},
{d: "bar.*", want: domainMatchTypePrefix},
{d: "*.abc.com", want: domainMatchTypeSuffix},
{d: "foo.bar.com", want: domainMatchTypeExact},
{d: "foo.*.com", want: domainMatchTypeInvalid},
}
for _, tt := range tests {
if got := matchTypeForDomain(tt.d); got != tt.want {
t.Errorf("matchTypeForDomain(%q) = %v, want %v", tt.d, got, tt.want)
}
}
}
func (s) TestMatch(t *testing.T) {
tests := []struct {
name string
domain string
host string
wantTyp domainMatchType
wantMatched bool
}{
{name: "invalid-empty", domain: "", host: "", wantTyp: domainMatchTypeInvalid, wantMatched: false},
{name: "invalid", domain: "a.*.b", host: "", wantTyp: domainMatchTypeInvalid, wantMatched: false},
{name: "universal", domain: "*", host: "abc.com", wantTyp: domainMatchTypeUniversal, wantMatched: true},
{name: "prefix-match", domain: "abc.*", host: "abc.123", wantTyp: domainMatchTypePrefix, wantMatched: true},
{name: "prefix-no-match", domain: "abc.*", host: "abcd.123", wantTyp: domainMatchTypePrefix, wantMatched: false},
{name: "suffix-match", domain: "*.123", host: "abc.123", wantTyp: domainMatchTypeSuffix, wantMatched: true},
{name: "suffix-no-match", domain: "*.123", host: "abc.1234", wantTyp: domainMatchTypeSuffix, wantMatched: false},
{name: "exact-match", domain: "foo.bar", host: "foo.bar", wantTyp: domainMatchTypeExact, wantMatched: true},
{name: "exact-no-match", domain: "foo.bar.com", host: "foo.bar", wantTyp: domainMatchTypeExact, wantMatched: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if gotTyp, gotMatched := match(tt.domain, tt.host); gotTyp != tt.wantTyp || gotMatched != tt.wantMatched {
t.Errorf("match() = %v, %v, want %v, %v", gotTyp, gotMatched, tt.wantTyp, tt.wantMatched)
}
})
}
}
func (s) TestFindBestMatchingVirtualHost(t *testing.T) {
var (
oneExactMatch = &v3routepb.VirtualHost{
Name: "one-exact-match",
Domains: []string{"foo.bar.com"},
}
oneSuffixMatch = &v3routepb.VirtualHost{
Name: "one-suffix-match",
Domains: []string{"*.bar.com"},
}
onePrefixMatch = &v3routepb.VirtualHost{
Name: "one-prefix-match",
Domains: []string{"foo.bar.*"},
}
oneUniversalMatch = &v3routepb.VirtualHost{
Name: "one-universal-match",
Domains: []string{"*"},
}
longExactMatch = &v3routepb.VirtualHost{
Name: "one-exact-match",
Domains: []string{"v2.foo.bar.com"},
}
multipleMatch = &v3routepb.VirtualHost{
Name: "multiple-match",
Domains: []string{"pi.foo.bar.com", "314.*", "*.159"},
}
vhs = []*v3routepb.VirtualHost{oneExactMatch, oneSuffixMatch, onePrefixMatch, oneUniversalMatch, longExactMatch, multipleMatch}
)
tests := []struct {
name string
host string
vHosts []*v3routepb.VirtualHost
want *v3routepb.VirtualHost
}{
{name: "exact-match", host: "foo.bar.com", vHosts: vhs, want: oneExactMatch},
{name: "suffix-match", host: "123.bar.com", vHosts: vhs, want: oneSuffixMatch},
{name: "prefix-match", host: "foo.bar.org", vHosts: vhs, want: onePrefixMatch},
{name: "universal-match", host: "abc.123", vHosts: vhs, want: oneUniversalMatch},
{name: "long-exact-match", host: "v2.foo.bar.com", vHosts: vhs, want: longExactMatch},
// Matches suffix "*.bar.com" and exact "pi.foo.bar.com". Takes exact.
{name: "multiple-match-exact", host: "pi.foo.bar.com", vHosts: vhs, want: multipleMatch},
// Matches suffix "*.159" and prefix "foo.bar.*". Takes suffix.
{name: "multiple-match-suffix", host: "foo.bar.159", vHosts: vhs, want: multipleMatch},
// Matches suffix "*.bar.com" and prefix "314.*". Takes suffix.
{name: "multiple-match-prefix", host: "314.bar.com", vHosts: vhs, want: oneSuffixMatch},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := findBestMatchingVirtualHost(tt.host, tt.vHosts); !cmp.Equal(got, tt.want, cmp.Comparer(proto.Equal)) {
t.Errorf("findBestMatchingVirtualHost() = %v, want %v", got, tt.want)
}
})
}
}
func (s) TestRoutesProtoToSlice(t *testing.T) {
tests := []struct {
name string
routes []*v3routepb.Route
wantRoutes []*Route
wantErr bool
}{
{
name: "no path",
routes: []*v3routepb.Route{{
Match: &v3routepb.RouteMatch{},
}},
wantErr: true,
},
{
name: "case_sensitive is false",
routes: []*v3routepb.Route{{
Match: &v3routepb.RouteMatch{
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
CaseSensitive: &wrapperspb.BoolValue{Value: false},
},
}},
wantErr: true,
},
{
name: "good",
routes: []*v3routepb.Route{
{
Match: &v3routepb.RouteMatch{
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
Headers: []*v3routepb.HeaderMatcher{
{
Name: "th",
HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{
PrefixMatch: "tv",
},
InvertMatch: true,
},
},
RuntimeFraction: &v3corepb.RuntimeFractionalPercent{
DefaultValue: &v3typepb.FractionalPercent{
Numerator: 1,
Denominator: v3typepb.FractionalPercent_HUNDRED,
},
},
},
Action: &v3routepb.Route_Route{
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
WeightedClusters: &v3routepb.WeightedCluster{
Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
},
TotalWeight: &wrapperspb.UInt32Value{Value: 100},
}}}},
},
},
wantRoutes: []*Route{{
Prefix: newStringP("/a/"),
Headers: []*HeaderMatcher{
{
Name: "th",
InvertMatch: newBoolP(true),
PrefixMatch: newStringP("tv"),
},
},
Fraction: newUInt32P(10000),
Action: map[string]uint32{"A": 40, "B": 60},
}},
wantErr: false,
},
{
name: "query is ignored",
routes: []*v3routepb.Route{
{
Match: &v3routepb.RouteMatch{
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
},
Action: &v3routepb.Route_Route{
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
WeightedClusters: &v3routepb.WeightedCluster{
Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
},
TotalWeight: &wrapperspb.UInt32Value{Value: 100},
}}}},
},
{
Name: "with_query",
Match: &v3routepb.RouteMatch{
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/b/"},
QueryParameters: []*v3routepb.QueryParameterMatcher{{Name: "route_will_be_ignored"}},
},
},
},
// Only one route in the result, because the second one with query
// parameters is ignored.
wantRoutes: []*Route{{
Prefix: newStringP("/a/"),
Action: map[string]uint32{"A": 40, "B": 60},
}},
wantErr: false,
},
}
cmpOpts := []cmp.Option{
cmp.AllowUnexported(Route{}, HeaderMatcher{}, Int64Range{}),
cmpopts.EquateEmpty(),
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := routesProtoToSlice(tt.routes, nil)
if (err != nil) != tt.wantErr {
t.Errorf("routesProtoToSlice() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !cmp.Equal(got, tt.wantRoutes, cmpOpts...) {
t.Errorf("routesProtoToSlice() got = %v, want %v, diff: %v", got, tt.wantRoutes, cmp.Diff(got, tt.wantRoutes, cmpOpts...))
}
})
}
}
func newStringP(s string) *string {
return &s
}
func newUInt32P(i uint32) *uint32 {
return &i
}
func newBoolP(b bool) *bool {
return &b
}

View File

@ -23,13 +23,10 @@ import (
"time" "time"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/internal/grpclog"
"google.golang.org/grpc/internal/grpctest" "google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/xds/internal/client/bootstrap" "google.golang.org/grpc/xds/internal/client/bootstrap"
"google.golang.org/grpc/xds/internal/testutils" "google.golang.org/grpc/xds/internal/testutils"
"google.golang.org/grpc/xds/internal/testutils/fakeserver" "google.golang.org/grpc/xds/internal/version"
corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
) )
type s struct { type s struct {
@ -60,119 +57,56 @@ func clientOpts(balancerName string) Options {
} }
} }
func (s) TestNew(t *testing.T) { type testAPIClient struct {
fakeServer, cleanup, err := fakeserver.StartServer() r UpdateHandler
if err != nil {
t.Fatalf("Failed to start fake xDS server: %v", err)
}
defer cleanup()
tests := []struct {
name string
opts Options
wantErr bool
}{
{name: "empty-opts", opts: Options{}, wantErr: true},
{
name: "empty-balancer-name",
opts: Options{
Config: bootstrap.Config{
Creds: grpc.WithInsecure(),
NodeProto: testutils.EmptyNodeProtoV2,
},
},
wantErr: true,
},
{
name: "empty-dial-creds",
opts: Options{
Config: bootstrap.Config{
BalancerName: "dummy",
NodeProto: testutils.EmptyNodeProtoV2,
},
},
wantErr: true,
},
{
name: "empty-node-proto",
opts: Options{
Config: bootstrap.Config{
BalancerName: "dummy",
Creds: grpc.WithInsecure(),
},
},
wantErr: true,
},
{
name: "happy-case",
opts: clientOpts(fakeServer.Address),
wantErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c, err := New(test.opts)
if err == nil {
defer c.Close()
}
if (err != nil) != test.wantErr {
t.Fatalf("New(%+v) = %v, wantErr: %v", test.opts, err, test.wantErr)
}
})
}
}
type testXDSV2Client struct {
r updateHandler
addWatches map[string]*testutils.Channel addWatches map[string]*testutils.Channel
removeWatches map[string]*testutils.Channel removeWatches map[string]*testutils.Channel
} }
func overrideNewXDSV2Client() (<-chan *testXDSV2Client, func()) { func overrideNewAPIClient() (<-chan *testAPIClient, func()) {
oldNewXDSV2Client := newXDSV2Client origNewAPIClient := newAPIClient
ch := make(chan *testXDSV2Client, 1) ch := make(chan *testAPIClient, 1)
newXDSV2Client = func(parent *Client, cc *grpc.ClientConn, nodeProto *corepb.Node, backoff func(int) time.Duration, logger *grpclog.PrefixLogger) xdsv2Client { newAPIClient = func(apiVersion version.TransportAPI, cc *grpc.ClientConn, opts BuildOptions) (APIClient, error) {
ret := newTestXDSV2Client(parent) ret := newTestAPIClient(opts.Parent)
ch <- ret ch <- ret
return ret return ret, nil
} }
return ch, func() { newXDSV2Client = oldNewXDSV2Client } return ch, func() { newAPIClient = origNewAPIClient }
} }
func newTestXDSV2Client(r updateHandler) *testXDSV2Client { func newTestAPIClient(r UpdateHandler) *testAPIClient {
addWatches := make(map[string]*testutils.Channel) addWatches := make(map[string]*testutils.Channel)
addWatches[ldsURL] = testutils.NewChannel() addWatches[version.V2ListenerURL] = testutils.NewChannel()
addWatches[rdsURL] = testutils.NewChannel() addWatches[version.V2RouteConfigURL] = testutils.NewChannel()
addWatches[cdsURL] = testutils.NewChannel() addWatches[version.V2ClusterURL] = testutils.NewChannel()
addWatches[edsURL] = testutils.NewChannel() addWatches[version.V2EndpointsURL] = testutils.NewChannel()
removeWatches := make(map[string]*testutils.Channel) removeWatches := make(map[string]*testutils.Channel)
removeWatches[ldsURL] = testutils.NewChannel() removeWatches[version.V2ListenerURL] = testutils.NewChannel()
removeWatches[rdsURL] = testutils.NewChannel() removeWatches[version.V2RouteConfigURL] = testutils.NewChannel()
removeWatches[cdsURL] = testutils.NewChannel() removeWatches[version.V2ClusterURL] = testutils.NewChannel()
removeWatches[edsURL] = testutils.NewChannel() removeWatches[version.V2EndpointsURL] = testutils.NewChannel()
return &testXDSV2Client{ return &testAPIClient{
r: r, r: r,
addWatches: addWatches, addWatches: addWatches,
removeWatches: removeWatches, removeWatches: removeWatches,
} }
} }
func (c *testXDSV2Client) addWatch(resourceType, resourceName string) { func (c *testAPIClient) AddWatch(resourceType, resourceName string) {
c.addWatches[resourceType].Send(resourceName) c.addWatches[resourceType].Send(resourceName)
} }
func (c *testXDSV2Client) removeWatch(resourceType, resourceName string) { func (c *testAPIClient) RemoveWatch(resourceType, resourceName string) {
c.removeWatches[resourceType].Send(resourceName) c.removeWatches[resourceType].Send(resourceName)
} }
func (c *testXDSV2Client) close() {} func (c *testAPIClient) Close() {}
// TestWatchCallAnotherWatch covers the case where watch() is called inline by a // TestWatchCallAnotherWatch covers the case where watch() is called inline by a
// callback. It makes sure it doesn't cause a deadlock. // callback. It makes sure it doesn't cause a deadlock.
func (s) TestWatchCallAnotherWatch(t *testing.T) { func (s) TestWatchCallAnotherWatch(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -189,17 +123,17 @@ func (s) TestWatchCallAnotherWatch(t *testing.T) {
clusterUpdateCh.Send(clusterUpdateErr{u: update, err: err}) clusterUpdateCh.Send(clusterUpdateErr{u: update, err: err})
// Calls another watch inline, to ensure there's deadlock. // Calls another watch inline, to ensure there's deadlock.
c.WatchCluster("another-random-name", func(ClusterUpdate, error) {}) c.WatchCluster("another-random-name", func(ClusterUpdate, error) {})
if _, err := v2Client.addWatches[cdsURL].Receive(); firstTime && err != nil { if _, err := v2Client.addWatches[version.V2ClusterURL].Receive(); firstTime && err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
firstTime = false firstTime = false
}) })
if _, err := v2Client.addWatches[cdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ClusterURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
wantUpdate := ClusterUpdate{ServiceName: testEDSName} wantUpdate := ClusterUpdate{ServiceName: testEDSName}
v2Client.r.newCDSUpdate(map[string]ClusterUpdate{ v2Client.r.NewClusters(map[string]ClusterUpdate{
testCDSName: wantUpdate, testCDSName: wantUpdate,
}) })
@ -208,7 +142,7 @@ func (s) TestWatchCallAnotherWatch(t *testing.T) {
} }
wantUpdate2 := ClusterUpdate{ServiceName: testEDSName + "2"} wantUpdate2 := ClusterUpdate{ServiceName: testEDSName + "2"}
v2Client.r.newCDSUpdate(map[string]ClusterUpdate{ v2Client.r.NewClusters(map[string]ClusterUpdate{
testCDSName: wantUpdate2, testCDSName: wantUpdate2,
}) })

View File

@ -22,19 +22,14 @@ import (
"fmt" "fmt"
"sync" "sync"
"time" "time"
"google.golang.org/grpc/xds/internal/version"
) )
// The value chosen here is based on the default value of the // The value chosen here is based on the default value of the
// initial_fetch_timeout field in corepb.ConfigSource proto. // initial_fetch_timeout field in corepb.ConfigSource proto.
var defaultWatchExpiryTimeout = 15 * time.Second var defaultWatchExpiryTimeout = 15 * time.Second
const (
ldsURL = "type.googleapis.com/envoy.api.v2.Listener"
rdsURL = "type.googleapis.com/envoy.api.v2.RouteConfiguration"
cdsURL = "type.googleapis.com/envoy.api.v2.Cluster"
edsURL = "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment"
)
type watchInfoState int type watchInfoState int
const ( const (
@ -50,8 +45,8 @@ type watchInfo struct {
typeURL string typeURL string
target string target string
ldsCallback ldsCallbackFunc ldsCallback func(ListenerUpdate, error)
rdsCallback rdsCallbackFunc rdsCallback func(RouteConfigUpdate, error)
cdsCallback func(ClusterUpdate, error) cdsCallback func(ClusterUpdate, error)
edsCallback func(EndpointsUpdate, error) edsCallback func(EndpointsUpdate, error)
@ -102,13 +97,13 @@ func (wi *watchInfo) sendErrorLocked(err error) {
u interface{} u interface{}
) )
switch wi.typeURL { switch wi.typeURL {
case ldsURL: case version.V2ListenerURL:
u = ldsUpdate{} u = ListenerUpdate{}
case rdsURL: case version.V2RouteConfigURL:
u = rdsUpdate{} u = RouteConfigUpdate{}
case cdsURL: case version.V2ClusterURL:
u = ClusterUpdate{} u = ClusterUpdate{}
case edsURL: case version.V2EndpointsURL:
u = EndpointsUpdate{} u = EndpointsUpdate{}
} }
wi.c.scheduleCallback(wi, u, err) wi.c.scheduleCallback(wi, u, err)
@ -130,13 +125,13 @@ func (c *Client) watch(wi *watchInfo) (cancel func()) {
c.logger.Debugf("new watch for type %v, resource name %v", wi.typeURL, wi.target) c.logger.Debugf("new watch for type %v, resource name %v", wi.typeURL, wi.target)
var watchers map[string]map[*watchInfo]bool var watchers map[string]map[*watchInfo]bool
switch wi.typeURL { switch wi.typeURL {
case ldsURL: case version.V2ListenerURL:
watchers = c.ldsWatchers watchers = c.ldsWatchers
case rdsURL: case version.V2RouteConfigURL:
watchers = c.rdsWatchers watchers = c.rdsWatchers
case cdsURL: case version.V2ClusterURL:
watchers = c.cdsWatchers watchers = c.cdsWatchers
case edsURL: case version.V2EndpointsURL:
watchers = c.edsWatchers watchers = c.edsWatchers
} }
@ -151,7 +146,7 @@ func (c *Client) watch(wi *watchInfo) (cancel func()) {
c.logger.Debugf("first watch for type %v, resource name %v, will send a new xDS request", wi.typeURL, wi.target) c.logger.Debugf("first watch for type %v, resource name %v, will send a new xDS request", wi.typeURL, wi.target)
s = make(map[*watchInfo]bool) s = make(map[*watchInfo]bool)
watchers[resourceName] = s watchers[resourceName] = s
c.v2c.addWatch(wi.typeURL, resourceName) c.apiClient.AddWatch(wi.typeURL, resourceName)
} }
// No matter what, add the new watcher to the set, so it's callback will be // No matter what, add the new watcher to the set, so it's callback will be
// call for new responses. // call for new responses.
@ -159,22 +154,22 @@ func (c *Client) watch(wi *watchInfo) (cancel func()) {
// If the resource is in cache, call the callback with the value. // If the resource is in cache, call the callback with the value.
switch wi.typeURL { switch wi.typeURL {
case ldsURL: case version.V2ListenerURL:
if v, ok := c.ldsCache[resourceName]; ok { if v, ok := c.ldsCache[resourceName]; ok {
c.logger.Debugf("LDS resource with name %v found in cache: %+v", wi.target, v) c.logger.Debugf("LDS resource with name %v found in cache: %+v", wi.target, v)
wi.newUpdate(v) wi.newUpdate(v)
} }
case rdsURL: case version.V2RouteConfigURL:
if v, ok := c.rdsCache[resourceName]; ok { if v, ok := c.rdsCache[resourceName]; ok {
c.logger.Debugf("RDS resource with name %v found in cache: %+v", wi.target, v) c.logger.Debugf("RDS resource with name %v found in cache: %+v", wi.target, v)
wi.newUpdate(v) wi.newUpdate(v)
} }
case cdsURL: case version.V2ClusterURL:
if v, ok := c.cdsCache[resourceName]; ok { if v, ok := c.cdsCache[resourceName]; ok {
c.logger.Debugf("CDS resource with name %v found in cache: %+v", wi.target, v) c.logger.Debugf("CDS resource with name %v found in cache: %+v", wi.target, v)
wi.newUpdate(v) wi.newUpdate(v)
} }
case edsURL: case version.V2EndpointsURL:
if v, ok := c.edsCache[resourceName]; ok { if v, ok := c.edsCache[resourceName]; ok {
c.logger.Debugf("EDS resource with name %v found in cache: %+v", wi.target, v) c.logger.Debugf("EDS resource with name %v found in cache: %+v", wi.target, v)
wi.newUpdate(v) wi.newUpdate(v)
@ -195,21 +190,207 @@ func (c *Client) watch(wi *watchInfo) (cancel func()) {
// If this was the last watcher, also tell xdsv2Client to stop // If this was the last watcher, also tell xdsv2Client to stop
// watching this resource. // watching this resource.
delete(watchers, resourceName) delete(watchers, resourceName)
c.v2c.removeWatch(wi.typeURL, resourceName) c.apiClient.RemoveWatch(wi.typeURL, resourceName)
// Remove the resource from cache. When a watch for this // Remove the resource from cache. When a watch for this
// resource is added later, it will trigger a xDS request with // resource is added later, it will trigger a xDS request with
// resource names, and client will receive new xDS responses. // resource names, and client will receive new xDS responses.
switch wi.typeURL { switch wi.typeURL {
case ldsURL: case version.V2ListenerURL:
delete(c.ldsCache, resourceName) delete(c.ldsCache, resourceName)
case rdsURL: case version.V2RouteConfigURL:
delete(c.rdsCache, resourceName) delete(c.rdsCache, resourceName)
case cdsURL: case version.V2ClusterURL:
delete(c.cdsCache, resourceName) delete(c.cdsCache, resourceName)
case edsURL: case version.V2EndpointsURL:
delete(c.edsCache, resourceName) delete(c.edsCache, resourceName)
} }
} }
} }
} }
} }
// watchLDS starts a listener watcher for the service..
//
// Note that during race (e.g. an xDS response is received while the user is
// calling cancel()), there's a small window where the callback can be called
// after the watcher is canceled. The caller needs to handle this case.
func (c *Client) watchLDS(serviceName string, cb func(ListenerUpdate, error)) (cancel func()) {
wi := &watchInfo{
c: c,
typeURL: version.V2ListenerURL,
target: serviceName,
ldsCallback: cb,
}
wi.expiryTimer = time.AfterFunc(defaultWatchExpiryTimeout, func() {
wi.timeout()
})
return c.watch(wi)
}
// watchRDS starts a listener watcher for the service..
//
// Note that during race (e.g. an xDS response is received while the user is
// calling cancel()), there's a small window where the callback can be called
// after the watcher is canceled. The caller needs to handle this case.
func (c *Client) watchRDS(routeName string, cb func(RouteConfigUpdate, error)) (cancel func()) {
wi := &watchInfo{
c: c,
typeURL: version.V2RouteConfigURL,
target: routeName,
rdsCallback: cb,
}
wi.expiryTimer = time.AfterFunc(defaultWatchExpiryTimeout, func() {
wi.timeout()
})
return c.watch(wi)
}
// WatchService uses LDS and RDS to discover information about the provided
// serviceName.
//
// WatchService can only be called once. The second call will not start a
// watcher and the callback will get an error. It's this case because an xDS
// client is expected to be used only by one ClientConn.
//
// Note that during race (e.g. an xDS response is received while the user is
// calling cancel()), there's a small window where the callback can be called
// after the watcher is canceled. The caller needs to handle this case.
func (c *Client) WatchService(serviceName string, cb func(ServiceUpdate, error)) (cancel func()) {
c.mu.Lock()
if len(c.ldsWatchers) != 0 {
go cb(ServiceUpdate{}, fmt.Errorf("unexpected WatchService when there's another service being watched"))
c.mu.Unlock()
return func() {}
}
c.mu.Unlock()
w := &serviceUpdateWatcher{c: c, serviceCb: cb}
w.ldsCancel = c.watchLDS(serviceName, w.handleLDSResp)
return w.close
}
// serviceUpdateWatcher handles LDS and RDS response, and calls the service
// callback at the right time.
type serviceUpdateWatcher struct {
c *Client
ldsCancel func()
serviceCb func(ServiceUpdate, error)
mu sync.Mutex
closed bool
rdsName string
rdsCancel func()
}
func (w *serviceUpdateWatcher) handleLDSResp(update ListenerUpdate, err error) {
w.c.logger.Infof("xds: client received LDS update: %+v, err: %v", update, err)
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return
}
if err != nil {
// We check the error type and do different things. For now, the only
// type we check is ResourceNotFound, which indicates the LDS resource
// was removed, and besides sending the error to callback, we also
// cancel the RDS watch.
if ErrType(err) == ErrorTypeResourceNotFound && w.rdsCancel != nil {
w.rdsCancel()
w.rdsName = ""
w.rdsCancel = nil
}
// The other error cases still return early without canceling the
// existing RDS watch.
w.serviceCb(ServiceUpdate{}, err)
return
}
if w.rdsName == update.RouteConfigName {
// If the new RouteConfigName is same as the previous, don't cancel and
// restart the RDS watch.
return
}
w.rdsName = update.RouteConfigName
if w.rdsCancel != nil {
w.rdsCancel()
}
w.rdsCancel = w.c.watchRDS(update.RouteConfigName, w.handleRDSResp)
}
func (w *serviceUpdateWatcher) handleRDSResp(update RouteConfigUpdate, err error) {
w.c.logger.Infof("xds: client received RDS update: %+v, err: %v", update, err)
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return
}
if w.rdsCancel == nil {
// This mean only the RDS watch is canceled, can happen if the LDS
// resource is removed.
return
}
if err != nil {
w.serviceCb(ServiceUpdate{}, err)
return
}
w.serviceCb(ServiceUpdate(update), nil)
}
func (w *serviceUpdateWatcher) close() {
w.mu.Lock()
defer w.mu.Unlock()
w.closed = true
w.ldsCancel()
if w.rdsCancel != nil {
w.rdsCancel()
w.rdsCancel = nil
}
}
// WatchCluster uses CDS to discover information about the provided
// clusterName.
//
// WatchCluster can be called multiple times, with same or different
// clusterNames. Each call will start an independent watcher for the resource.
//
// Note that during race (e.g. an xDS response is received while the user is
// calling cancel()), there's a small window where the callback can be called
// after the watcher is canceled. The caller needs to handle this case.
func (c *Client) WatchCluster(clusterName string, cb func(ClusterUpdate, error)) (cancel func()) {
wi := &watchInfo{
c: c,
typeURL: version.V2ClusterURL,
target: clusterName,
cdsCallback: cb,
}
wi.expiryTimer = time.AfterFunc(defaultWatchExpiryTimeout, func() {
wi.timeout()
})
return c.watch(wi)
}
// WatchEndpoints uses EDS to discover endpoints in the provided clusterName.
//
// WatchEndpoints can be called multiple times, with same or different
// clusterNames. Each call will start an independent watcher for the resource.
//
// Note that during race (e.g. an xDS response is received while the user is
// calling cancel()), there's a small window where the callback can be called
// after the watcher is canceled. The caller needs to handle this case.
func (c *Client) WatchEndpoints(clusterName string, cb func(EndpointsUpdate, error)) (cancel func()) {
wi := &watchInfo{
c: c,
typeURL: version.V2EndpointsURL,
target: clusterName,
edsCallback: cb,
}
wi.expiryTimer = time.AfterFunc(defaultWatchExpiryTimeout, func() {
wi.timeout()
})
return c.watch(wi)
}

View File

@ -1,56 +0,0 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package client
import (
"time"
)
// ClusterUpdate contains information from a received CDS response, which is of
// interest to the registered CDS watcher.
type ClusterUpdate struct {
// ServiceName is the service name corresponding to the clusterName which
// is being watched for through CDS.
ServiceName string
// EnableLRS indicates whether or not load should be reported through LRS.
EnableLRS bool
}
// WatchCluster uses CDS to discover information about the provided
// clusterName.
//
// WatchCluster can be called multiple times, with same or different
// clusterNames. Each call will start an independent watcher for the resource.
//
// Note that during race (e.g. an xDS response is received while the user is
// calling cancel()), there's a small window where the callback can be called
// after the watcher is canceled. The caller needs to handle this case.
func (c *Client) WatchCluster(clusterName string, cb func(ClusterUpdate, error)) (cancel func()) {
wi := &watchInfo{
c: c,
typeURL: cdsURL,
target: clusterName,
cdsCallback: cb,
}
wi.expiryTimer = time.AfterFunc(defaultWatchExpiryTimeout, func() {
wi.timeout()
})
return c.watch(wi)
}

View File

@ -23,6 +23,7 @@ import (
"time" "time"
"google.golang.org/grpc/xds/internal/testutils" "google.golang.org/grpc/xds/internal/testutils"
"google.golang.org/grpc/xds/internal/version"
) )
type clusterUpdateErr struct { type clusterUpdateErr struct {
@ -35,7 +36,7 @@ type clusterUpdateErr struct {
// - an update for another resource name // - an update for another resource name
// - an update is received after cancel() // - an update is received after cancel()
func (s) TestClusterWatch(t *testing.T) { func (s) TestClusterWatch(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -53,7 +54,7 @@ func (s) TestClusterWatch(t *testing.T) {
cancelWatch := c.WatchCluster(testCDSName, func(update ClusterUpdate, err error) { cancelWatch := c.WatchCluster(testCDSName, func(update ClusterUpdate, err error) {
clusterUpdateCh.Send(clusterUpdateErr{u: update, err: err}) clusterUpdateCh.Send(clusterUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[cdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ClusterURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
@ -64,7 +65,7 @@ func (s) TestClusterWatch(t *testing.T) {
// //
// TODO: in a future cleanup, this (and the same thing in other tests) can // TODO: in a future cleanup, this (and the same thing in other tests) can
// be changed call Client directly. // be changed call Client directly.
v2Client.r.newCDSUpdate(map[string]ClusterUpdate{ v2Client.r.NewClusters(map[string]ClusterUpdate{
testCDSName: wantUpdate, testCDSName: wantUpdate,
}) })
@ -73,7 +74,7 @@ func (s) TestClusterWatch(t *testing.T) {
} }
// Another update, with an extra resource for a different resource name. // Another update, with an extra resource for a different resource name.
v2Client.r.newCDSUpdate(map[string]ClusterUpdate{ v2Client.r.NewClusters(map[string]ClusterUpdate{
testCDSName: wantUpdate, testCDSName: wantUpdate,
"randomName": {}, "randomName": {},
}) })
@ -84,7 +85,7 @@ func (s) TestClusterWatch(t *testing.T) {
// Cancel watch, and send update again. // Cancel watch, and send update again.
cancelWatch() cancelWatch()
v2Client.r.newCDSUpdate(map[string]ClusterUpdate{ v2Client.r.NewClusters(map[string]ClusterUpdate{
testCDSName: wantUpdate, testCDSName: wantUpdate,
}) })
@ -96,7 +97,7 @@ func (s) TestClusterWatch(t *testing.T) {
// TestClusterTwoWatchSameResourceName covers the case where an update is received // TestClusterTwoWatchSameResourceName covers the case where an update is received
// after two watch() for the same resource name. // after two watch() for the same resource name.
func (s) TestClusterTwoWatchSameResourceName(t *testing.T) { func (s) TestClusterTwoWatchSameResourceName(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -118,13 +119,13 @@ func (s) TestClusterTwoWatchSameResourceName(t *testing.T) {
cancelLastWatch = c.WatchCluster(testCDSName, func(update ClusterUpdate, err error) { cancelLastWatch = c.WatchCluster(testCDSName, func(update ClusterUpdate, err error) {
clusterUpdateCh.Send(clusterUpdateErr{u: update, err: err}) clusterUpdateCh.Send(clusterUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[cdsURL].Receive(); i == 0 && err != nil { if _, err := v2Client.addWatches[version.V2ClusterURL].Receive(); i == 0 && err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
} }
wantUpdate := ClusterUpdate{ServiceName: testEDSName} wantUpdate := ClusterUpdate{ServiceName: testEDSName}
v2Client.r.newCDSUpdate(map[string]ClusterUpdate{ v2Client.r.NewClusters(map[string]ClusterUpdate{
testCDSName: wantUpdate, testCDSName: wantUpdate,
}) })
@ -136,7 +137,7 @@ func (s) TestClusterTwoWatchSameResourceName(t *testing.T) {
// Cancel the last watch, and send update again. // Cancel the last watch, and send update again.
cancelLastWatch() cancelLastWatch()
v2Client.r.newCDSUpdate(map[string]ClusterUpdate{ v2Client.r.NewClusters(map[string]ClusterUpdate{
testCDSName: wantUpdate, testCDSName: wantUpdate,
}) })
@ -154,7 +155,7 @@ func (s) TestClusterTwoWatchSameResourceName(t *testing.T) {
// TestClusterThreeWatchDifferentResourceName covers the case where an update is // TestClusterThreeWatchDifferentResourceName covers the case where an update is
// received after three watch() for different resource names. // received after three watch() for different resource names.
func (s) TestClusterThreeWatchDifferentResourceName(t *testing.T) { func (s) TestClusterThreeWatchDifferentResourceName(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -175,7 +176,7 @@ func (s) TestClusterThreeWatchDifferentResourceName(t *testing.T) {
c.WatchCluster(testCDSName+"1", func(update ClusterUpdate, err error) { c.WatchCluster(testCDSName+"1", func(update ClusterUpdate, err error) {
clusterUpdateCh.Send(clusterUpdateErr{u: update, err: err}) clusterUpdateCh.Send(clusterUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[cdsURL].Receive(); i == 0 && err != nil { if _, err := v2Client.addWatches[version.V2ClusterURL].Receive(); i == 0 && err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
} }
@ -185,13 +186,13 @@ func (s) TestClusterThreeWatchDifferentResourceName(t *testing.T) {
c.WatchCluster(testCDSName+"2", func(update ClusterUpdate, err error) { c.WatchCluster(testCDSName+"2", func(update ClusterUpdate, err error) {
clusterUpdateCh2.Send(clusterUpdateErr{u: update, err: err}) clusterUpdateCh2.Send(clusterUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[cdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ClusterURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
wantUpdate1 := ClusterUpdate{ServiceName: testEDSName + "1"} wantUpdate1 := ClusterUpdate{ServiceName: testEDSName + "1"}
wantUpdate2 := ClusterUpdate{ServiceName: testEDSName + "2"} wantUpdate2 := ClusterUpdate{ServiceName: testEDSName + "2"}
v2Client.r.newCDSUpdate(map[string]ClusterUpdate{ v2Client.r.NewClusters(map[string]ClusterUpdate{
testCDSName + "1": wantUpdate1, testCDSName + "1": wantUpdate1,
testCDSName + "2": wantUpdate2, testCDSName + "2": wantUpdate2,
}) })
@ -210,7 +211,7 @@ func (s) TestClusterThreeWatchDifferentResourceName(t *testing.T) {
// TestClusterWatchAfterCache covers the case where watch is called after the update // TestClusterWatchAfterCache covers the case where watch is called after the update
// is in cache. // is in cache.
func (s) TestClusterWatchAfterCache(t *testing.T) { func (s) TestClusterWatchAfterCache(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -225,12 +226,12 @@ func (s) TestClusterWatchAfterCache(t *testing.T) {
c.WatchCluster(testCDSName, func(update ClusterUpdate, err error) { c.WatchCluster(testCDSName, func(update ClusterUpdate, err error) {
clusterUpdateCh.Send(clusterUpdateErr{u: update, err: err}) clusterUpdateCh.Send(clusterUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[cdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ClusterURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
wantUpdate := ClusterUpdate{ServiceName: testEDSName} wantUpdate := ClusterUpdate{ServiceName: testEDSName}
v2Client.r.newCDSUpdate(map[string]ClusterUpdate{ v2Client.r.NewClusters(map[string]ClusterUpdate{
testCDSName: wantUpdate, testCDSName: wantUpdate,
}) })
@ -243,7 +244,7 @@ func (s) TestClusterWatchAfterCache(t *testing.T) {
c.WatchCluster(testCDSName, func(update ClusterUpdate, err error) { c.WatchCluster(testCDSName, func(update ClusterUpdate, err error) {
clusterUpdateCh2.Send(clusterUpdateErr{u: update, err: err}) clusterUpdateCh2.Send(clusterUpdateErr{u: update, err: err})
}) })
if n, err := v2Client.addWatches[cdsURL].Receive(); err == nil { if n, err := v2Client.addWatches[version.V2ClusterURL].Receive(); err == nil {
t.Fatalf("want no new watch to start (recv timeout), got resource name: %v error %v", n, err) t.Fatalf("want no new watch to start (recv timeout), got resource name: %v error %v", n, err)
} }
@ -268,7 +269,7 @@ func (s) TestClusterWatchExpiryTimer(t *testing.T) {
defaultWatchExpiryTimeout = oldWatchExpiryTimeout defaultWatchExpiryTimeout = oldWatchExpiryTimeout
}() }()
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -283,7 +284,7 @@ func (s) TestClusterWatchExpiryTimer(t *testing.T) {
c.WatchCluster(testCDSName, func(u ClusterUpdate, err error) { c.WatchCluster(testCDSName, func(u ClusterUpdate, err error) {
clusterUpdateCh.Send(clusterUpdateErr{u: u, err: err}) clusterUpdateCh.Send(clusterUpdateErr{u: u, err: err})
}) })
if _, err := v2Client.addWatches[cdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ClusterURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
@ -310,7 +311,7 @@ func (s) TestClusterWatchExpiryTimerStop(t *testing.T) {
defaultWatchExpiryTimeout = oldWatchExpiryTimeout defaultWatchExpiryTimeout = oldWatchExpiryTimeout
}() }()
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -325,12 +326,12 @@ func (s) TestClusterWatchExpiryTimerStop(t *testing.T) {
c.WatchCluster(testCDSName, func(u ClusterUpdate, err error) { c.WatchCluster(testCDSName, func(u ClusterUpdate, err error) {
clusterUpdateCh.Send(clusterUpdateErr{u: u, err: err}) clusterUpdateCh.Send(clusterUpdateErr{u: u, err: err})
}) })
if _, err := v2Client.addWatches[cdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ClusterURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
wantUpdate := ClusterUpdate{ServiceName: testEDSName} wantUpdate := ClusterUpdate{ServiceName: testEDSName}
v2Client.r.newCDSUpdate(map[string]ClusterUpdate{ v2Client.r.NewClusters(map[string]ClusterUpdate{
testCDSName: wantUpdate, testCDSName: wantUpdate,
}) })
@ -352,7 +353,7 @@ func (s) TestClusterWatchExpiryTimerStop(t *testing.T) {
// - one more update without the removed resource // - one more update without the removed resource
// - the callback (above) shouldn't receive any update // - the callback (above) shouldn't receive any update
func (s) TestClusterResourceRemoved(t *testing.T) { func (s) TestClusterResourceRemoved(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -367,7 +368,7 @@ func (s) TestClusterResourceRemoved(t *testing.T) {
c.WatchCluster(testCDSName+"1", func(update ClusterUpdate, err error) { c.WatchCluster(testCDSName+"1", func(update ClusterUpdate, err error) {
clusterUpdateCh1.Send(clusterUpdateErr{u: update, err: err}) clusterUpdateCh1.Send(clusterUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[cdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ClusterURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
// Another watch for a different name. // Another watch for a different name.
@ -375,13 +376,13 @@ func (s) TestClusterResourceRemoved(t *testing.T) {
c.WatchCluster(testCDSName+"2", func(update ClusterUpdate, err error) { c.WatchCluster(testCDSName+"2", func(update ClusterUpdate, err error) {
clusterUpdateCh2.Send(clusterUpdateErr{u: update, err: err}) clusterUpdateCh2.Send(clusterUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[cdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ClusterURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
wantUpdate1 := ClusterUpdate{ServiceName: testEDSName + "1"} wantUpdate1 := ClusterUpdate{ServiceName: testEDSName + "1"}
wantUpdate2 := ClusterUpdate{ServiceName: testEDSName + "2"} wantUpdate2 := ClusterUpdate{ServiceName: testEDSName + "2"}
v2Client.r.newCDSUpdate(map[string]ClusterUpdate{ v2Client.r.NewClusters(map[string]ClusterUpdate{
testCDSName + "1": wantUpdate1, testCDSName + "1": wantUpdate1,
testCDSName + "2": wantUpdate2, testCDSName + "2": wantUpdate2,
}) })
@ -395,7 +396,7 @@ func (s) TestClusterResourceRemoved(t *testing.T) {
} }
// Send another update to remove resource 1. // Send another update to remove resource 1.
v2Client.r.newCDSUpdate(map[string]ClusterUpdate{ v2Client.r.NewClusters(map[string]ClusterUpdate{
testCDSName + "2": wantUpdate2, testCDSName + "2": wantUpdate2,
}) })
@ -410,7 +411,7 @@ func (s) TestClusterResourceRemoved(t *testing.T) {
} }
// Send one more update without resource 1. // Send one more update without resource 1.
v2Client.r.newCDSUpdate(map[string]ClusterUpdate{ v2Client.r.NewClusters(map[string]ClusterUpdate{
testCDSName + "2": wantUpdate2, testCDSName + "2": wantUpdate2,
}) })

View File

@ -1,93 +0,0 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package client
import (
"time"
"google.golang.org/grpc/xds/internal"
)
// OverloadDropConfig contains the config to drop overloads.
type OverloadDropConfig struct {
Category string
Numerator uint32
Denominator uint32
}
// EndpointHealthStatus represents the health status of an endpoint.
type EndpointHealthStatus int32
const (
// EndpointHealthStatusUnknown represents HealthStatus UNKNOWN.
EndpointHealthStatusUnknown EndpointHealthStatus = iota
// EndpointHealthStatusHealthy represents HealthStatus HEALTHY.
EndpointHealthStatusHealthy
// EndpointHealthStatusUnhealthy represents HealthStatus UNHEALTHY.
EndpointHealthStatusUnhealthy
// EndpointHealthStatusDraining represents HealthStatus DRAINING.
EndpointHealthStatusDraining
// EndpointHealthStatusTimeout represents HealthStatus TIMEOUT.
EndpointHealthStatusTimeout
// EndpointHealthStatusDegraded represents HealthStatus DEGRADED.
EndpointHealthStatusDegraded
)
// Endpoint contains information of an endpoint.
type Endpoint struct {
Address string
HealthStatus EndpointHealthStatus
Weight uint32
}
// Locality contains information of a locality.
type Locality struct {
Endpoints []Endpoint
ID internal.LocalityID
Priority uint32
Weight uint32
}
// EndpointsUpdate contains an EDS update.
type EndpointsUpdate struct {
Drops []OverloadDropConfig
Localities []Locality
}
// WatchEndpoints uses EDS to discover endpoints in the provided clusterName.
//
// WatchEndpoints can be called multiple times, with same or different
// clusterNames. Each call will start an independent watcher for the resource.
//
// Note that during race (e.g. an xDS response is received while the user is
// calling cancel()), there's a small window where the callback can be called
// after the watcher is canceled. The caller needs to handle this case.
func (c *Client) WatchEndpoints(clusterName string, cb func(EndpointsUpdate, error)) (cancel func()) {
wi := &watchInfo{
c: c,
typeURL: edsURL,
target: clusterName,
edsCallback: cb,
}
wi.expiryTimer = time.AfterFunc(defaultWatchExpiryTimeout, func() {
wi.timeout()
})
return c.watch(wi)
}

View File

@ -24,9 +24,9 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/grpc/xds/internal" "google.golang.org/grpc/xds/internal"
"google.golang.org/grpc/xds/internal/testutils" "google.golang.org/grpc/xds/internal/testutils"
"google.golang.org/grpc/xds/internal/version"
) )
var ( var (
@ -57,7 +57,7 @@ type endpointsUpdateErr struct {
// - an update for another resource name (which doesn't trigger callback) // - an update for another resource name (which doesn't trigger callback)
// - an update is received after cancel() // - an update is received after cancel()
func (s) TestEndpointsWatch(t *testing.T) { func (s) TestEndpointsWatch(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -72,12 +72,12 @@ func (s) TestEndpointsWatch(t *testing.T) {
cancelWatch := c.WatchEndpoints(testCDSName, func(update EndpointsUpdate, err error) { cancelWatch := c.WatchEndpoints(testCDSName, func(update EndpointsUpdate, err error) {
endpointsUpdateCh.Send(endpointsUpdateErr{u: update, err: err}) endpointsUpdateCh.Send(endpointsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[edsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2EndpointsURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
wantUpdate := EndpointsUpdate{Localities: []Locality{testLocalities[0]}} wantUpdate := EndpointsUpdate{Localities: []Locality{testLocalities[0]}}
v2Client.r.newEDSUpdate(map[string]EndpointsUpdate{ v2Client.r.NewEndpoints(map[string]EndpointsUpdate{
testCDSName: wantUpdate, testCDSName: wantUpdate,
}) })
@ -86,7 +86,7 @@ func (s) TestEndpointsWatch(t *testing.T) {
} }
// Another update for a different resource name. // Another update for a different resource name.
v2Client.r.newEDSUpdate(map[string]EndpointsUpdate{ v2Client.r.NewEndpoints(map[string]EndpointsUpdate{
"randomName": {}, "randomName": {},
}) })
@ -96,7 +96,7 @@ func (s) TestEndpointsWatch(t *testing.T) {
// Cancel watch, and send update again. // Cancel watch, and send update again.
cancelWatch() cancelWatch()
v2Client.r.newEDSUpdate(map[string]EndpointsUpdate{ v2Client.r.NewEndpoints(map[string]EndpointsUpdate{
testCDSName: wantUpdate, testCDSName: wantUpdate,
}) })
@ -108,7 +108,7 @@ func (s) TestEndpointsWatch(t *testing.T) {
// TestEndpointsTwoWatchSameResourceName covers the case where an update is received // TestEndpointsTwoWatchSameResourceName covers the case where an update is received
// after two watch() for the same resource name. // after two watch() for the same resource name.
func (s) TestEndpointsTwoWatchSameResourceName(t *testing.T) { func (s) TestEndpointsTwoWatchSameResourceName(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -130,13 +130,13 @@ func (s) TestEndpointsTwoWatchSameResourceName(t *testing.T) {
cancelLastWatch = c.WatchEndpoints(testCDSName, func(update EndpointsUpdate, err error) { cancelLastWatch = c.WatchEndpoints(testCDSName, func(update EndpointsUpdate, err error) {
endpointsUpdateCh.Send(endpointsUpdateErr{u: update, err: err}) endpointsUpdateCh.Send(endpointsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[edsURL].Receive(); i == 0 && err != nil { if _, err := v2Client.addWatches[version.V2EndpointsURL].Receive(); i == 0 && err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
} }
wantUpdate := EndpointsUpdate{Localities: []Locality{testLocalities[0]}} wantUpdate := EndpointsUpdate{Localities: []Locality{testLocalities[0]}}
v2Client.r.newEDSUpdate(map[string]EndpointsUpdate{ v2Client.r.NewEndpoints(map[string]EndpointsUpdate{
testCDSName: wantUpdate, testCDSName: wantUpdate,
}) })
@ -148,7 +148,7 @@ func (s) TestEndpointsTwoWatchSameResourceName(t *testing.T) {
// Cancel the last watch, and send update again. // Cancel the last watch, and send update again.
cancelLastWatch() cancelLastWatch()
v2Client.r.newEDSUpdate(map[string]EndpointsUpdate{ v2Client.r.NewEndpoints(map[string]EndpointsUpdate{
testCDSName: wantUpdate, testCDSName: wantUpdate,
}) })
@ -166,7 +166,7 @@ func (s) TestEndpointsTwoWatchSameResourceName(t *testing.T) {
// TestEndpointsThreeWatchDifferentResourceName covers the case where an update is // TestEndpointsThreeWatchDifferentResourceName covers the case where an update is
// received after three watch() for different resource names. // received after three watch() for different resource names.
func (s) TestEndpointsThreeWatchDifferentResourceName(t *testing.T) { func (s) TestEndpointsThreeWatchDifferentResourceName(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -187,7 +187,7 @@ func (s) TestEndpointsThreeWatchDifferentResourceName(t *testing.T) {
c.WatchEndpoints(testCDSName+"1", func(update EndpointsUpdate, err error) { c.WatchEndpoints(testCDSName+"1", func(update EndpointsUpdate, err error) {
endpointsUpdateCh.Send(endpointsUpdateErr{u: update, err: err}) endpointsUpdateCh.Send(endpointsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[edsURL].Receive(); i == 0 && err != nil { if _, err := v2Client.addWatches[version.V2EndpointsURL].Receive(); i == 0 && err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
} }
@ -197,13 +197,13 @@ func (s) TestEndpointsThreeWatchDifferentResourceName(t *testing.T) {
c.WatchEndpoints(testCDSName+"2", func(update EndpointsUpdate, err error) { c.WatchEndpoints(testCDSName+"2", func(update EndpointsUpdate, err error) {
endpointsUpdateCh2.Send(endpointsUpdateErr{u: update, err: err}) endpointsUpdateCh2.Send(endpointsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[edsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2EndpointsURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
wantUpdate1 := EndpointsUpdate{Localities: []Locality{testLocalities[0]}} wantUpdate1 := EndpointsUpdate{Localities: []Locality{testLocalities[0]}}
wantUpdate2 := EndpointsUpdate{Localities: []Locality{testLocalities[1]}} wantUpdate2 := EndpointsUpdate{Localities: []Locality{testLocalities[1]}}
v2Client.r.newEDSUpdate(map[string]EndpointsUpdate{ v2Client.r.NewEndpoints(map[string]EndpointsUpdate{
testCDSName + "1": wantUpdate1, testCDSName + "1": wantUpdate1,
testCDSName + "2": wantUpdate2, testCDSName + "2": wantUpdate2,
}) })
@ -222,7 +222,7 @@ func (s) TestEndpointsThreeWatchDifferentResourceName(t *testing.T) {
// TestEndpointsWatchAfterCache covers the case where watch is called after the update // TestEndpointsWatchAfterCache covers the case where watch is called after the update
// is in cache. // is in cache.
func (s) TestEndpointsWatchAfterCache(t *testing.T) { func (s) TestEndpointsWatchAfterCache(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -237,12 +237,12 @@ func (s) TestEndpointsWatchAfterCache(t *testing.T) {
c.WatchEndpoints(testCDSName, func(update EndpointsUpdate, err error) { c.WatchEndpoints(testCDSName, func(update EndpointsUpdate, err error) {
endpointsUpdateCh.Send(endpointsUpdateErr{u: update, err: err}) endpointsUpdateCh.Send(endpointsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[edsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2EndpointsURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
wantUpdate := EndpointsUpdate{Localities: []Locality{testLocalities[0]}} wantUpdate := EndpointsUpdate{Localities: []Locality{testLocalities[0]}}
v2Client.r.newEDSUpdate(map[string]EndpointsUpdate{ v2Client.r.NewEndpoints(map[string]EndpointsUpdate{
testCDSName: wantUpdate, testCDSName: wantUpdate,
}) })
@ -255,7 +255,7 @@ func (s) TestEndpointsWatchAfterCache(t *testing.T) {
c.WatchEndpoints(testCDSName, func(update EndpointsUpdate, err error) { c.WatchEndpoints(testCDSName, func(update EndpointsUpdate, err error) {
endpointsUpdateCh2.Send(endpointsUpdateErr{u: update, err: err}) endpointsUpdateCh2.Send(endpointsUpdateErr{u: update, err: err})
}) })
if n, err := v2Client.addWatches[edsURL].Receive(); err == nil { if n, err := v2Client.addWatches[version.V2EndpointsURL].Receive(); err == nil {
t.Fatalf("want no new watch to start (recv timeout), got resource name: %v error %v", n, err) t.Fatalf("want no new watch to start (recv timeout), got resource name: %v error %v", n, err)
} }
@ -280,7 +280,7 @@ func (s) TestEndpointsWatchExpiryTimer(t *testing.T) {
defaultWatchExpiryTimeout = oldWatchExpiryTimeout defaultWatchExpiryTimeout = oldWatchExpiryTimeout
}() }()
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -295,7 +295,7 @@ func (s) TestEndpointsWatchExpiryTimer(t *testing.T) {
c.WatchEndpoints(testCDSName, func(update EndpointsUpdate, err error) { c.WatchEndpoints(testCDSName, func(update EndpointsUpdate, err error) {
endpointsUpdateCh.Send(endpointsUpdateErr{u: update, err: err}) endpointsUpdateCh.Send(endpointsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[edsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2EndpointsURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }

View File

@ -1,47 +0,0 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package client
import (
"time"
)
type ldsUpdate struct {
routeName string
}
type ldsCallbackFunc func(ldsUpdate, error)
// watchLDS starts a listener watcher for the service..
//
// Note that during race (e.g. an xDS response is received while the user is
// calling cancel()), there's a small window where the callback can be called
// after the watcher is canceled. The caller needs to handle this case.
func (c *Client) watchLDS(serviceName string, cb ldsCallbackFunc) (cancel func()) {
wi := &watchInfo{
c: c,
typeURL: ldsURL,
target: serviceName,
ldsCallback: cb,
}
wi.expiryTimer = time.AfterFunc(defaultWatchExpiryTimeout, func() {
wi.timeout()
})
return c.watch(wi)
}

View File

@ -22,10 +22,11 @@ import (
"testing" "testing"
"google.golang.org/grpc/xds/internal/testutils" "google.golang.org/grpc/xds/internal/testutils"
"google.golang.org/grpc/xds/internal/version"
) )
type ldsUpdateErr struct { type ldsUpdateErr struct {
u ldsUpdate u ListenerUpdate
err error err error
} }
@ -34,7 +35,7 @@ type ldsUpdateErr struct {
// - an update for another resource name // - an update for another resource name
// - an update is received after cancel() // - an update is received after cancel()
func (s) TestLDSWatch(t *testing.T) { func (s) TestLDSWatch(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -46,47 +47,47 @@ func (s) TestLDSWatch(t *testing.T) {
v2Client := <-v2ClientCh v2Client := <-v2ClientCh
ldsUpdateCh := testutils.NewChannel() ldsUpdateCh := testutils.NewChannel()
cancelWatch := c.watchLDS(testLDSName, func(update ldsUpdate, err error) { cancelWatch := c.watchLDS(testLDSName, func(update ListenerUpdate, err error) {
ldsUpdateCh.Send(ldsUpdateErr{u: update, err: err}) ldsUpdateCh.Send(ldsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[ldsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ListenerURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
wantUpdate := ldsUpdate{routeName: testRDSName} wantUpdate := ListenerUpdate{RouteConfigName: testRDSName}
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: wantUpdate, testLDSName: wantUpdate,
}) })
if u, err := ldsUpdateCh.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate, nil}) { if u, err := ldsUpdateCh.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate, nil}) {
t.Errorf("unexpected ldsUpdate: %v, error receiving from channel: %v", u, err) t.Errorf("unexpected ListenerUpdate: %v, error receiving from channel: %v", u, err)
} }
// Another update, with an extra resource for a different resource name. // Another update, with an extra resource for a different resource name.
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: wantUpdate, testLDSName: wantUpdate,
"randomName": {}, "randomName": {},
}) })
if u, err := ldsUpdateCh.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate, nil}) { if u, err := ldsUpdateCh.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate, nil}) {
t.Errorf("unexpected ldsUpdate: %v, %v, want channel recv timeout", u, err) t.Errorf("unexpected ListenerUpdate: %v, %v, want channel recv timeout", u, err)
} }
// Cancel watch, and send update again. // Cancel watch, and send update again.
cancelWatch() cancelWatch()
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: wantUpdate, testLDSName: wantUpdate,
}) })
if u, err := ldsUpdateCh.TimedReceive(chanRecvTimeout); err != testutils.ErrRecvTimeout { if u, err := ldsUpdateCh.TimedReceive(chanRecvTimeout); err != testutils.ErrRecvTimeout {
t.Errorf("unexpected ldsUpdate: %v, %v, want channel recv timeout", u, err) t.Errorf("unexpected ListenerUpdate: %v, %v, want channel recv timeout", u, err)
} }
} }
// TestLDSTwoWatchSameResourceName covers the case where an update is received // TestLDSTwoWatchSameResourceName covers the case where an update is received
// after two watch() for the same resource name. // after two watch() for the same resource name.
func (s) TestLDSTwoWatchSameResourceName(t *testing.T) { func (s) TestLDSTwoWatchSameResourceName(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -105,46 +106,46 @@ func (s) TestLDSTwoWatchSameResourceName(t *testing.T) {
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
ldsUpdateCh := testutils.NewChannel() ldsUpdateCh := testutils.NewChannel()
ldsUpdateChs = append(ldsUpdateChs, ldsUpdateCh) ldsUpdateChs = append(ldsUpdateChs, ldsUpdateCh)
cancelLastWatch = c.watchLDS(testLDSName, func(update ldsUpdate, err error) { cancelLastWatch = c.watchLDS(testLDSName, func(update ListenerUpdate, err error) {
ldsUpdateCh.Send(ldsUpdateErr{u: update, err: err}) ldsUpdateCh.Send(ldsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[ldsURL].Receive(); i == 0 && err != nil { if _, err := v2Client.addWatches[version.V2ListenerURL].Receive(); i == 0 && err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
} }
wantUpdate := ldsUpdate{routeName: testRDSName} wantUpdate := ListenerUpdate{RouteConfigName: testRDSName}
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: wantUpdate, testLDSName: wantUpdate,
}) })
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
if u, err := ldsUpdateChs[i].Receive(); err != nil || u != (ldsUpdateErr{wantUpdate, nil}) { if u, err := ldsUpdateChs[i].Receive(); err != nil || u != (ldsUpdateErr{wantUpdate, nil}) {
t.Errorf("i=%v, unexpected ldsUpdate: %v, error receiving from channel: %v", i, u, err) t.Errorf("i=%v, unexpected ListenerUpdate: %v, error receiving from channel: %v", i, u, err)
} }
} }
// Cancel the last watch, and send update again. // Cancel the last watch, and send update again.
cancelLastWatch() cancelLastWatch()
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: wantUpdate, testLDSName: wantUpdate,
}) })
for i := 0; i < count-1; i++ { for i := 0; i < count-1; i++ {
if u, err := ldsUpdateChs[i].Receive(); err != nil || u != (ldsUpdateErr{wantUpdate, nil}) { if u, err := ldsUpdateChs[i].Receive(); err != nil || u != (ldsUpdateErr{wantUpdate, nil}) {
t.Errorf("i=%v, unexpected ldsUpdate: %v, error receiving from channel: %v", i, u, err) t.Errorf("i=%v, unexpected ListenerUpdate: %v, error receiving from channel: %v", i, u, err)
} }
} }
if u, err := ldsUpdateChs[count-1].TimedReceive(chanRecvTimeout); err != testutils.ErrRecvTimeout { if u, err := ldsUpdateChs[count-1].TimedReceive(chanRecvTimeout); err != testutils.ErrRecvTimeout {
t.Errorf("unexpected ldsUpdate: %v, %v, want channel recv timeout", u, err) t.Errorf("unexpected ListenerUpdate: %v, %v, want channel recv timeout", u, err)
} }
} }
// TestLDSThreeWatchDifferentResourceName covers the case where an update is // TestLDSThreeWatchDifferentResourceName covers the case where an update is
// received after three watch() for different resource names. // received after three watch() for different resource names.
func (s) TestLDSThreeWatchDifferentResourceName(t *testing.T) { func (s) TestLDSThreeWatchDifferentResourceName(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -162,45 +163,45 @@ func (s) TestLDSThreeWatchDifferentResourceName(t *testing.T) {
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
ldsUpdateCh := testutils.NewChannel() ldsUpdateCh := testutils.NewChannel()
ldsUpdateChs = append(ldsUpdateChs, ldsUpdateCh) ldsUpdateChs = append(ldsUpdateChs, ldsUpdateCh)
c.watchLDS(testLDSName+"1", func(update ldsUpdate, err error) { c.watchLDS(testLDSName+"1", func(update ListenerUpdate, err error) {
ldsUpdateCh.Send(ldsUpdateErr{u: update, err: err}) ldsUpdateCh.Send(ldsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[ldsURL].Receive(); i == 0 && err != nil { if _, err := v2Client.addWatches[version.V2ListenerURL].Receive(); i == 0 && err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
} }
// Third watch for a different name. // Third watch for a different name.
ldsUpdateCh2 := testutils.NewChannel() ldsUpdateCh2 := testutils.NewChannel()
c.watchLDS(testLDSName+"2", func(update ldsUpdate, err error) { c.watchLDS(testLDSName+"2", func(update ListenerUpdate, err error) {
ldsUpdateCh2.Send(ldsUpdateErr{u: update, err: err}) ldsUpdateCh2.Send(ldsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[ldsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ListenerURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
wantUpdate1 := ldsUpdate{routeName: testRDSName + "1"} wantUpdate1 := ListenerUpdate{RouteConfigName: testRDSName + "1"}
wantUpdate2 := ldsUpdate{routeName: testRDSName + "2"} wantUpdate2 := ListenerUpdate{RouteConfigName: testRDSName + "2"}
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName + "1": wantUpdate1, testLDSName + "1": wantUpdate1,
testLDSName + "2": wantUpdate2, testLDSName + "2": wantUpdate2,
}) })
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
if u, err := ldsUpdateChs[i].Receive(); err != nil || u != (ldsUpdateErr{wantUpdate1, nil}) { if u, err := ldsUpdateChs[i].Receive(); err != nil || u != (ldsUpdateErr{wantUpdate1, nil}) {
t.Errorf("i=%v, unexpected ldsUpdate: %v, error receiving from channel: %v", i, u, err) t.Errorf("i=%v, unexpected ListenerUpdate: %v, error receiving from channel: %v", i, u, err)
} }
} }
if u, err := ldsUpdateCh2.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate2, nil}) { if u, err := ldsUpdateCh2.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate2, nil}) {
t.Errorf("unexpected ldsUpdate: %v, error receiving from channel: %v", u, err) t.Errorf("unexpected ListenerUpdate: %v, error receiving from channel: %v", u, err)
} }
} }
// TestLDSWatchAfterCache covers the case where watch is called after the update // TestLDSWatchAfterCache covers the case where watch is called after the update
// is in cache. // is in cache.
func (s) TestLDSWatchAfterCache(t *testing.T) { func (s) TestLDSWatchAfterCache(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -212,39 +213,39 @@ func (s) TestLDSWatchAfterCache(t *testing.T) {
v2Client := <-v2ClientCh v2Client := <-v2ClientCh
ldsUpdateCh := testutils.NewChannel() ldsUpdateCh := testutils.NewChannel()
c.watchLDS(testLDSName, func(update ldsUpdate, err error) { c.watchLDS(testLDSName, func(update ListenerUpdate, err error) {
ldsUpdateCh.Send(ldsUpdateErr{u: update, err: err}) ldsUpdateCh.Send(ldsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[ldsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ListenerURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
wantUpdate := ldsUpdate{routeName: testRDSName} wantUpdate := ListenerUpdate{RouteConfigName: testRDSName}
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: wantUpdate, testLDSName: wantUpdate,
}) })
if u, err := ldsUpdateCh.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate, nil}) { if u, err := ldsUpdateCh.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate, nil}) {
t.Errorf("unexpected ldsUpdate: %v, error receiving from channel: %v", u, err) t.Errorf("unexpected ListenerUpdate: %v, error receiving from channel: %v", u, err)
} }
// Another watch for the resource in cache. // Another watch for the resource in cache.
ldsUpdateCh2 := testutils.NewChannel() ldsUpdateCh2 := testutils.NewChannel()
c.watchLDS(testLDSName, func(update ldsUpdate, err error) { c.watchLDS(testLDSName, func(update ListenerUpdate, err error) {
ldsUpdateCh2.Send(ldsUpdateErr{u: update, err: err}) ldsUpdateCh2.Send(ldsUpdateErr{u: update, err: err})
}) })
if n, err := v2Client.addWatches[ldsURL].Receive(); err == nil { if n, err := v2Client.addWatches[version.V2ListenerURL].Receive(); err == nil {
t.Fatalf("want no new watch to start (recv timeout), got resource name: %v error %v", n, err) t.Fatalf("want no new watch to start (recv timeout), got resource name: %v error %v", n, err)
} }
// New watch should receives the update. // New watch should receives the update.
if u, err := ldsUpdateCh2.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate, nil}) { if u, err := ldsUpdateCh2.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate, nil}) {
t.Errorf("unexpected ldsUpdate: %v, error receiving from channel: %v", u, err) t.Errorf("unexpected ListenerUpdate: %v, error receiving from channel: %v", u, err)
} }
// Old watch should see nothing. // Old watch should see nothing.
if u, err := ldsUpdateCh.TimedReceive(chanRecvTimeout); err != testutils.ErrRecvTimeout { if u, err := ldsUpdateCh.TimedReceive(chanRecvTimeout); err != testutils.ErrRecvTimeout {
t.Errorf("unexpected ldsUpdate: %v, %v, want channel recv timeout", u, err) t.Errorf("unexpected ListenerUpdate: %v, %v, want channel recv timeout", u, err)
} }
} }
@ -255,7 +256,7 @@ func (s) TestLDSWatchAfterCache(t *testing.T) {
// - one more update without the removed resource // - one more update without the removed resource
// - the callback (above) shouldn't receive any update // - the callback (above) shouldn't receive any update
func (s) TestLDSResourceRemoved(t *testing.T) { func (s) TestLDSResourceRemoved(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -267,63 +268,63 @@ func (s) TestLDSResourceRemoved(t *testing.T) {
v2Client := <-v2ClientCh v2Client := <-v2ClientCh
ldsUpdateCh1 := testutils.NewChannel() ldsUpdateCh1 := testutils.NewChannel()
c.watchLDS(testLDSName+"1", func(update ldsUpdate, err error) { c.watchLDS(testLDSName+"1", func(update ListenerUpdate, err error) {
ldsUpdateCh1.Send(ldsUpdateErr{u: update, err: err}) ldsUpdateCh1.Send(ldsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[ldsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ListenerURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
// Another watch for a different name. // Another watch for a different name.
ldsUpdateCh2 := testutils.NewChannel() ldsUpdateCh2 := testutils.NewChannel()
c.watchLDS(testLDSName+"2", func(update ldsUpdate, err error) { c.watchLDS(testLDSName+"2", func(update ListenerUpdate, err error) {
ldsUpdateCh2.Send(ldsUpdateErr{u: update, err: err}) ldsUpdateCh2.Send(ldsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[ldsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ListenerURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
wantUpdate1 := ldsUpdate{routeName: testEDSName + "1"} wantUpdate1 := ListenerUpdate{RouteConfigName: testEDSName + "1"}
wantUpdate2 := ldsUpdate{routeName: testEDSName + "2"} wantUpdate2 := ListenerUpdate{RouteConfigName: testEDSName + "2"}
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName + "1": wantUpdate1, testLDSName + "1": wantUpdate1,
testLDSName + "2": wantUpdate2, testLDSName + "2": wantUpdate2,
}) })
if u, err := ldsUpdateCh1.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate1, nil}) { if u, err := ldsUpdateCh1.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate1, nil}) {
t.Errorf("unexpected ldsUpdate: %v, error receiving from channel: %v", u, err) t.Errorf("unexpected ListenerUpdate: %v, error receiving from channel: %v", u, err)
} }
if u, err := ldsUpdateCh2.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate2, nil}) { if u, err := ldsUpdateCh2.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate2, nil}) {
t.Errorf("unexpected ldsUpdate: %v, error receiving from channel: %v", u, err) t.Errorf("unexpected ListenerUpdate: %v, error receiving from channel: %v", u, err)
} }
// Send another update to remove resource 1. // Send another update to remove resource 1.
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName + "2": wantUpdate2, testLDSName + "2": wantUpdate2,
}) })
// watcher 1 should get an error. // watcher 1 should get an error.
if u, err := ldsUpdateCh1.Receive(); err != nil || ErrType(u.(ldsUpdateErr).err) != ErrorTypeResourceNotFound { if u, err := ldsUpdateCh1.Receive(); err != nil || ErrType(u.(ldsUpdateErr).err) != ErrorTypeResourceNotFound {
t.Errorf("unexpected ldsUpdate: %v, error receiving from channel: %v, want update with error resource not found", u, err) t.Errorf("unexpected ListenerUpdate: %v, error receiving from channel: %v, want update with error resource not found", u, err)
} }
// watcher 2 should get the same update again. // watcher 2 should get the same update again.
if u, err := ldsUpdateCh2.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate2, nil}) { if u, err := ldsUpdateCh2.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate2, nil}) {
t.Errorf("unexpected ldsUpdate: %v, error receiving from channel: %v", u, err) t.Errorf("unexpected ListenerUpdate: %v, error receiving from channel: %v", u, err)
} }
// Send one more update without resource 1. // Send one more update without resource 1.
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName + "2": wantUpdate2, testLDSName + "2": wantUpdate2,
}) })
// watcher 1 should get an error. // watcher 1 should get an error.
if u, err := ldsUpdateCh1.Receive(); err != testutils.ErrRecvTimeout { if u, err := ldsUpdateCh1.Receive(); err != testutils.ErrRecvTimeout {
t.Errorf("unexpected ldsUpdate: %v, want receiving from channel timeout", u) t.Errorf("unexpected ListenerUpdate: %v, want receiving from channel timeout", u)
} }
// watcher 2 should get the same update again. // watcher 2 should get the same update again.
if u, err := ldsUpdateCh2.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate2, nil}) { if u, err := ldsUpdateCh2.Receive(); err != nil || u != (ldsUpdateErr{wantUpdate2, nil}) {
t.Errorf("unexpected ldsUpdate: %v, error receiving from channel: %v", u, err) t.Errorf("unexpected ListenerUpdate: %v, error receiving from channel: %v", u, err)
} }
} }

View File

@ -1,73 +0,0 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package client
import (
"time"
)
// Int64Range is a range for header range match.
type Int64Range struct {
Start int64 `json:"start"`
End int64 `json:"end"`
}
// HeaderMatcher represents header matchers.
type HeaderMatcher struct {
Name string `json:"name"`
InvertMatch *bool `json:"invertMatch,omitempty"`
ExactMatch *string `json:"exactMatch,omitempty"`
RegexMatch *string `json:"regexMatch,omitempty"`
PrefixMatch *string `json:"prefixMatch,omitempty"`
SuffixMatch *string `json:"suffixMatch,omitempty"`
RangeMatch *Int64Range `json:"rangeMatch,omitempty"`
PresentMatch *bool `json:"presentMatch,omitempty"`
}
// Route represents route with matchers and action.
type Route struct {
Path, Prefix, Regex *string
Headers []*HeaderMatcher
Fraction *uint32
Action map[string]uint32 // action is weighted clusters.
}
type rdsUpdate struct {
routes []*Route
}
type rdsCallbackFunc func(rdsUpdate, error)
// watchRDS starts a listener watcher for the service..
//
// Note that during race (e.g. an xDS response is received while the user is
// calling cancel()), there's a small window where the callback can be called
// after the watcher is canceled. The caller needs to handle this case.
func (c *Client) watchRDS(routeName string, cb rdsCallbackFunc) (cancel func()) {
wi := &watchInfo{
c: c,
typeURL: rdsURL,
target: routeName,
rdsCallback: cb,
}
wi.expiryTimer = time.AfterFunc(defaultWatchExpiryTimeout, func() {
wi.timeout()
})
return c.watch(wi)
}

View File

@ -23,10 +23,11 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"google.golang.org/grpc/xds/internal/testutils" "google.golang.org/grpc/xds/internal/testutils"
"google.golang.org/grpc/xds/internal/version"
) )
type rdsUpdateErr struct { type rdsUpdateErr struct {
u rdsUpdate u RouteConfigUpdate
err error err error
} }
@ -35,7 +36,7 @@ type rdsUpdateErr struct {
// - an update for another resource name (which doesn't trigger callback) // - an update for another resource name (which doesn't trigger callback)
// - an update is received after cancel() // - an update is received after cancel()
func (s) TestRDSWatch(t *testing.T) { func (s) TestRDSWatch(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -47,46 +48,46 @@ func (s) TestRDSWatch(t *testing.T) {
v2Client := <-v2ClientCh v2Client := <-v2ClientCh
rdsUpdateCh := testutils.NewChannel() rdsUpdateCh := testutils.NewChannel()
cancelWatch := c.watchRDS(testRDSName, func(update rdsUpdate, err error) { cancelWatch := c.watchRDS(testRDSName, func(update RouteConfigUpdate, err error) {
rdsUpdateCh.Send(rdsUpdateErr{u: update, err: err}) rdsUpdateCh.Send(rdsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[rdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2RouteConfigURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
wantUpdate := rdsUpdate{routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}} wantUpdate := RouteConfigUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}}
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName: wantUpdate, testRDSName: wantUpdate,
}) })
if u, err := rdsUpdateCh.Receive(); err != nil || !cmp.Equal(u, rdsUpdateErr{wantUpdate, nil}, cmp.AllowUnexported(rdsUpdate{}, rdsUpdateErr{})) { if u, err := rdsUpdateCh.Receive(); err != nil || !cmp.Equal(u, rdsUpdateErr{wantUpdate, nil}, cmp.AllowUnexported(rdsUpdateErr{})) {
t.Errorf("unexpected rdsUpdate: %v, error receiving from channel: %v", u, err) t.Errorf("unexpected RouteConfigUpdate: %v, error receiving from channel: %v", u, err)
} }
// Another update for a different resource name. // Another update for a different resource name.
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
"randomName": {}, "randomName": {},
}) })
if u, err := rdsUpdateCh.TimedReceive(chanRecvTimeout); err != testutils.ErrRecvTimeout { if u, err := rdsUpdateCh.TimedReceive(chanRecvTimeout); err != testutils.ErrRecvTimeout {
t.Errorf("unexpected rdsUpdate: %v, %v, want channel recv timeout", u, err) t.Errorf("unexpected RouteConfigUpdate: %v, %v, want channel recv timeout", u, err)
} }
// Cancel watch, and send update again. // Cancel watch, and send update again.
cancelWatch() cancelWatch()
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName: wantUpdate, testRDSName: wantUpdate,
}) })
if u, err := rdsUpdateCh.TimedReceive(chanRecvTimeout); err != testutils.ErrRecvTimeout { if u, err := rdsUpdateCh.TimedReceive(chanRecvTimeout); err != testutils.ErrRecvTimeout {
t.Errorf("unexpected rdsUpdate: %v, %v, want channel recv timeout", u, err) t.Errorf("unexpected RouteConfigUpdate: %v, %v, want channel recv timeout", u, err)
} }
} }
// TestRDSTwoWatchSameResourceName covers the case where an update is received // TestRDSTwoWatchSameResourceName covers the case where an update is received
// after two watch() for the same resource name. // after two watch() for the same resource name.
func (s) TestRDSTwoWatchSameResourceName(t *testing.T) { func (s) TestRDSTwoWatchSameResourceName(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -105,46 +106,46 @@ func (s) TestRDSTwoWatchSameResourceName(t *testing.T) {
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
rdsUpdateCh := testutils.NewChannel() rdsUpdateCh := testutils.NewChannel()
rdsUpdateChs = append(rdsUpdateChs, rdsUpdateCh) rdsUpdateChs = append(rdsUpdateChs, rdsUpdateCh)
cancelLastWatch = c.watchRDS(testRDSName, func(update rdsUpdate, err error) { cancelLastWatch = c.watchRDS(testRDSName, func(update RouteConfigUpdate, err error) {
rdsUpdateCh.Send(rdsUpdateErr{u: update, err: err}) rdsUpdateCh.Send(rdsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[rdsURL].Receive(); i == 0 && err != nil { if _, err := v2Client.addWatches[version.V2RouteConfigURL].Receive(); i == 0 && err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
} }
wantUpdate := rdsUpdate{routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}} wantUpdate := RouteConfigUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}}
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName: wantUpdate, testRDSName: wantUpdate,
}) })
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
if u, err := rdsUpdateChs[i].Receive(); err != nil || !cmp.Equal(u, rdsUpdateErr{wantUpdate, nil}, cmp.AllowUnexported(rdsUpdate{}, rdsUpdateErr{})) { if u, err := rdsUpdateChs[i].Receive(); err != nil || !cmp.Equal(u, rdsUpdateErr{wantUpdate, nil}, cmp.AllowUnexported(rdsUpdateErr{})) {
t.Errorf("i=%v, unexpected rdsUpdate: %v, error receiving from channel: %v", i, u, err) t.Errorf("i=%v, unexpected RouteConfigUpdate: %v, error receiving from channel: %v", i, u, err)
} }
} }
// Cancel the last watch, and send update again. // Cancel the last watch, and send update again.
cancelLastWatch() cancelLastWatch()
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName: wantUpdate, testRDSName: wantUpdate,
}) })
for i := 0; i < count-1; i++ { for i := 0; i < count-1; i++ {
if u, err := rdsUpdateChs[i].Receive(); err != nil || !cmp.Equal(u, rdsUpdateErr{wantUpdate, nil}, cmp.AllowUnexported(rdsUpdate{}, rdsUpdateErr{})) { if u, err := rdsUpdateChs[i].Receive(); err != nil || !cmp.Equal(u, rdsUpdateErr{wantUpdate, nil}, cmp.AllowUnexported(rdsUpdateErr{})) {
t.Errorf("i=%v, unexpected rdsUpdate: %v, error receiving from channel: %v", i, u, err) t.Errorf("i=%v, unexpected RouteConfigUpdate: %v, error receiving from channel: %v", i, u, err)
} }
} }
if u, err := rdsUpdateChs[count-1].TimedReceive(chanRecvTimeout); err != testutils.ErrRecvTimeout { if u, err := rdsUpdateChs[count-1].TimedReceive(chanRecvTimeout); err != testutils.ErrRecvTimeout {
t.Errorf("unexpected rdsUpdate: %v, %v, want channel recv timeout", u, err) t.Errorf("unexpected RouteConfigUpdate: %v, %v, want channel recv timeout", u, err)
} }
} }
// TestRDSThreeWatchDifferentResourceName covers the case where an update is // TestRDSThreeWatchDifferentResourceName covers the case where an update is
// received after three watch() for different resource names. // received after three watch() for different resource names.
func (s) TestRDSThreeWatchDifferentResourceName(t *testing.T) { func (s) TestRDSThreeWatchDifferentResourceName(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -162,45 +163,45 @@ func (s) TestRDSThreeWatchDifferentResourceName(t *testing.T) {
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
rdsUpdateCh := testutils.NewChannel() rdsUpdateCh := testutils.NewChannel()
rdsUpdateChs = append(rdsUpdateChs, rdsUpdateCh) rdsUpdateChs = append(rdsUpdateChs, rdsUpdateCh)
c.watchRDS(testRDSName+"1", func(update rdsUpdate, err error) { c.watchRDS(testRDSName+"1", func(update RouteConfigUpdate, err error) {
rdsUpdateCh.Send(rdsUpdateErr{u: update, err: err}) rdsUpdateCh.Send(rdsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[rdsURL].Receive(); i == 0 && err != nil { if _, err := v2Client.addWatches[version.V2RouteConfigURL].Receive(); i == 0 && err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
} }
// Third watch for a different name. // Third watch for a different name.
rdsUpdateCh2 := testutils.NewChannel() rdsUpdateCh2 := testutils.NewChannel()
c.watchRDS(testRDSName+"2", func(update rdsUpdate, err error) { c.watchRDS(testRDSName+"2", func(update RouteConfigUpdate, err error) {
rdsUpdateCh2.Send(rdsUpdateErr{u: update, err: err}) rdsUpdateCh2.Send(rdsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[rdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2RouteConfigURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
wantUpdate1 := rdsUpdate{routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName + "1": 1}}}} wantUpdate1 := RouteConfigUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName + "1": 1}}}}
wantUpdate2 := rdsUpdate{routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName + "2": 1}}}} wantUpdate2 := RouteConfigUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName + "2": 1}}}}
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName + "1": wantUpdate1, testRDSName + "1": wantUpdate1,
testRDSName + "2": wantUpdate2, testRDSName + "2": wantUpdate2,
}) })
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
if u, err := rdsUpdateChs[i].Receive(); err != nil || !cmp.Equal(u, rdsUpdateErr{wantUpdate1, nil}, cmp.AllowUnexported(rdsUpdate{}, rdsUpdateErr{})) { if u, err := rdsUpdateChs[i].Receive(); err != nil || !cmp.Equal(u, rdsUpdateErr{wantUpdate1, nil}, cmp.AllowUnexported(rdsUpdateErr{})) {
t.Errorf("i=%v, unexpected rdsUpdate: %v, error receiving from channel: %v", i, u, err) t.Errorf("i=%v, unexpected RouteConfigUpdate: %v, error receiving from channel: %v", i, u, err)
} }
} }
if u, err := rdsUpdateCh2.Receive(); err != nil || !cmp.Equal(u, rdsUpdateErr{wantUpdate2, nil}, cmp.AllowUnexported(rdsUpdate{}, rdsUpdateErr{})) { if u, err := rdsUpdateCh2.Receive(); err != nil || !cmp.Equal(u, rdsUpdateErr{wantUpdate2, nil}, cmp.AllowUnexported(rdsUpdateErr{})) {
t.Errorf("unexpected rdsUpdate: %v, error receiving from channel: %v", u, err) t.Errorf("unexpected RouteConfigUpdate: %v, error receiving from channel: %v", u, err)
} }
} }
// TestRDSWatchAfterCache covers the case where watch is called after the update // TestRDSWatchAfterCache covers the case where watch is called after the update
// is in cache. // is in cache.
func (s) TestRDSWatchAfterCache(t *testing.T) { func (s) TestRDSWatchAfterCache(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -212,38 +213,38 @@ func (s) TestRDSWatchAfterCache(t *testing.T) {
v2Client := <-v2ClientCh v2Client := <-v2ClientCh
rdsUpdateCh := testutils.NewChannel() rdsUpdateCh := testutils.NewChannel()
c.watchRDS(testRDSName, func(update rdsUpdate, err error) { c.watchRDS(testRDSName, func(update RouteConfigUpdate, err error) {
rdsUpdateCh.Send(rdsUpdateErr{u: update, err: err}) rdsUpdateCh.Send(rdsUpdateErr{u: update, err: err})
}) })
if _, err := v2Client.addWatches[rdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2RouteConfigURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
wantUpdate := rdsUpdate{routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}} wantUpdate := RouteConfigUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}}
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName: wantUpdate, testRDSName: wantUpdate,
}) })
if u, err := rdsUpdateCh.Receive(); err != nil || !cmp.Equal(u, rdsUpdateErr{wantUpdate, nil}, cmp.AllowUnexported(rdsUpdate{}, rdsUpdateErr{})) { if u, err := rdsUpdateCh.Receive(); err != nil || !cmp.Equal(u, rdsUpdateErr{wantUpdate, nil}, cmp.AllowUnexported(rdsUpdateErr{})) {
t.Errorf("unexpected rdsUpdate: %v, error receiving from channel: %v", u, err) t.Errorf("unexpected RouteConfigUpdate: %v, error receiving from channel: %v", u, err)
} }
// Another watch for the resource in cache. // Another watch for the resource in cache.
rdsUpdateCh2 := testutils.NewChannel() rdsUpdateCh2 := testutils.NewChannel()
c.watchRDS(testRDSName, func(update rdsUpdate, err error) { c.watchRDS(testRDSName, func(update RouteConfigUpdate, err error) {
rdsUpdateCh2.Send(rdsUpdateErr{u: update, err: err}) rdsUpdateCh2.Send(rdsUpdateErr{u: update, err: err})
}) })
if n, err := v2Client.addWatches[rdsURL].Receive(); err == nil { if n, err := v2Client.addWatches[version.V2RouteConfigURL].Receive(); err == nil {
t.Fatalf("want no new watch to start (recv timeout), got resource name: %v error %v", n, err) t.Fatalf("want no new watch to start (recv timeout), got resource name: %v error %v", n, err)
} }
// New watch should receives the update. // New watch should receives the update.
if u, err := rdsUpdateCh2.Receive(); err != nil || !cmp.Equal(u, rdsUpdateErr{wantUpdate, nil}, cmp.AllowUnexported(rdsUpdate{}, rdsUpdateErr{})) { if u, err := rdsUpdateCh2.Receive(); err != nil || !cmp.Equal(u, rdsUpdateErr{wantUpdate, nil}, cmp.AllowUnexported(rdsUpdateErr{})) {
t.Errorf("unexpected rdsUpdate: %v, error receiving from channel: %v", u, err) t.Errorf("unexpected RouteConfigUpdate: %v, error receiving from channel: %v", u, err)
} }
// Old watch should see nothing. // Old watch should see nothing.
if u, err := rdsUpdateCh.TimedReceive(chanRecvTimeout); err != testutils.ErrRecvTimeout { if u, err := rdsUpdateCh.TimedReceive(chanRecvTimeout); err != testutils.ErrRecvTimeout {
t.Errorf("unexpected rdsUpdate: %v, %v, want channel recv timeout", u, err) t.Errorf("unexpected RouteConfigUpdate: %v, %v, want channel recv timeout", u, err)
} }
} }

View File

@ -1,135 +0,0 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package client
import (
"fmt"
"sync"
)
// ServiceUpdate contains update about the service.
type ServiceUpdate struct {
// Routes contain matchers+actions to route RPCs.
Routes []*Route
}
// WatchService uses LDS and RDS to discover information about the provided
// serviceName.
//
// WatchService can only be called once. The second call will not start a
// watcher and the callback will get an error. It's this case because an xDS
// client is expected to be used only by one ClientConn.
//
// Note that during race (e.g. an xDS response is received while the user is
// calling cancel()), there's a small window where the callback can be called
// after the watcher is canceled. The caller needs to handle this case.
func (c *Client) WatchService(serviceName string, cb func(ServiceUpdate, error)) (cancel func()) {
c.mu.Lock()
if len(c.ldsWatchers) != 0 {
go cb(ServiceUpdate{}, fmt.Errorf("unexpected WatchService when there's another service being watched"))
c.mu.Unlock()
return func() {}
}
c.mu.Unlock()
w := &serviceUpdateWatcher{c: c, serviceCb: cb}
w.ldsCancel = c.watchLDS(serviceName, w.handleLDSResp)
return w.close
}
// serviceUpdateWatcher handles LDS and RDS response, and calls the service
// callback at the right time.
type serviceUpdateWatcher struct {
c *Client
ldsCancel func()
serviceCb func(ServiceUpdate, error)
mu sync.Mutex
closed bool
rdsName string
rdsCancel func()
}
func (w *serviceUpdateWatcher) handleLDSResp(update ldsUpdate, err error) {
w.c.logger.Infof("xds: client received LDS update: %+v, err: %v", update, err)
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return
}
if err != nil {
// We check the error type and do different things. For now, the only
// type we check is ResourceNotFound, which indicates the LDS resource
// was removed, and besides sending the error to callback, we also
// cancel the RDS watch.
if ErrType(err) == ErrorTypeResourceNotFound && w.rdsCancel != nil {
w.rdsCancel()
w.rdsName = ""
w.rdsCancel = nil
}
// The other error cases still return early without canceling the
// existing RDS watch.
w.serviceCb(ServiceUpdate{}, err)
return
}
if w.rdsName == update.routeName {
// If the new routeName is same as the previous, don't cancel and
// restart the RDS watch.
return
}
w.rdsName = update.routeName
if w.rdsCancel != nil {
w.rdsCancel()
}
w.rdsCancel = w.c.watchRDS(update.routeName, w.handleRDSResp)
}
func (w *serviceUpdateWatcher) handleRDSResp(update rdsUpdate, err error) {
w.c.logger.Infof("xds: client received RDS update: %+v, err: %v", update, err)
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return
}
if w.rdsCancel == nil {
// This mean only the RDS watch is canceled, can happen if the LDS
// resource is removed.
return
}
if err != nil {
w.serviceCb(ServiceUpdate{}, err)
return
}
w.serviceCb(ServiceUpdate{
Routes: update.routes,
}, nil)
}
func (w *serviceUpdateWatcher) close() {
w.mu.Lock()
defer w.mu.Unlock()
w.closed = true
w.ldsCancel()
if w.rdsCancel != nil {
w.rdsCancel()
w.rdsCancel = nil
}
}

View File

@ -19,16 +19,13 @@
package client package client
import ( import (
"errors"
"fmt"
"testing" "testing"
"time" "time"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/grpc/xds/internal/testutils" "google.golang.org/grpc/xds/internal/testutils"
"google.golang.org/grpc/xds/internal/testutils/fakeserver" "google.golang.org/grpc/xds/internal/version"
) )
type serviceUpdateErr struct { type serviceUpdateErr struct {
@ -42,7 +39,7 @@ var serviceCmpOpts = []cmp.Option{cmp.AllowUnexported(serviceUpdateErr{}), cmpop
// - an update is received after a watch() // - an update is received after a watch()
// - an update with routes received // - an update with routes received
func (s) TestServiceWatch(t *testing.T) { func (s) TestServiceWatch(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -60,17 +57,17 @@ func (s) TestServiceWatch(t *testing.T) {
wantUpdate := ServiceUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}} wantUpdate := ServiceUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}}
if _, err := v2Client.addWatches[ldsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ListenerURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: {routeName: testRDSName}, testLDSName: {RouteConfigName: testRDSName},
}) })
if _, err := v2Client.addWatches[rdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2RouteConfigURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName: {routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}}, testRDSName: {Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}},
}) })
if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{wantUpdate, nil}, serviceCmpOpts...) { if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{wantUpdate, nil}, serviceCmpOpts...) {
@ -83,9 +80,9 @@ func (s) TestServiceWatch(t *testing.T) {
Action: map[string]uint32{testCDSName: 1}, Action: map[string]uint32{testCDSName: 1},
}}, }},
} }
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName: { testRDSName: {
routes: []*Route{{ Routes: []*Route{{
Prefix: newStringP(""), Prefix: newStringP(""),
Action: map[string]uint32{testCDSName: 1}, Action: map[string]uint32{testCDSName: 1},
}}, }},
@ -100,7 +97,7 @@ func (s) TestServiceWatch(t *testing.T) {
// response, the second LDS response trigger an new RDS watch, and an update of // response, the second LDS response trigger an new RDS watch, and an update of
// the old RDS watch doesn't trigger update to service callback. // the old RDS watch doesn't trigger update to service callback.
func (s) TestServiceWatchLDSUpdate(t *testing.T) { func (s) TestServiceWatchLDSUpdate(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -118,17 +115,17 @@ func (s) TestServiceWatchLDSUpdate(t *testing.T) {
wantUpdate := ServiceUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}} wantUpdate := ServiceUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}}
if _, err := v2Client.addWatches[ldsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ListenerURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: {routeName: testRDSName}, testLDSName: {RouteConfigName: testRDSName},
}) })
if _, err := v2Client.addWatches[rdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2RouteConfigURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName: {routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}}, testRDSName: {Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}},
}) })
if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{wantUpdate, nil}, serviceCmpOpts...) { if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{wantUpdate, nil}, serviceCmpOpts...) {
@ -136,16 +133,16 @@ func (s) TestServiceWatchLDSUpdate(t *testing.T) {
} }
// Another LDS update with a different RDS_name. // Another LDS update with a different RDS_name.
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: {routeName: testRDSName + "2"}, testLDSName: {RouteConfigName: testRDSName + "2"},
}) })
if _, err := v2Client.addWatches[rdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2RouteConfigURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
// Another update for the old name. // Another update for the old name.
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName: {routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}}, testRDSName: {Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}},
}) })
if u, err := serviceUpdateCh.Receive(); err != testutils.ErrRecvTimeout { if u, err := serviceUpdateCh.Receive(); err != testutils.ErrRecvTimeout {
@ -154,8 +151,8 @@ func (s) TestServiceWatchLDSUpdate(t *testing.T) {
wantUpdate2 := ServiceUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName + "2": 1}}}} wantUpdate2 := ServiceUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName + "2": 1}}}}
// RDS update for the new name. // RDS update for the new name.
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName + "2": {routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName + "2": 1}}}}, testRDSName + "2": {Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName + "2": 1}}}},
}) })
if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{wantUpdate2, nil}, serviceCmpOpts...) { if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{wantUpdate2, nil}, serviceCmpOpts...) {
@ -167,7 +164,7 @@ func (s) TestServiceWatchLDSUpdate(t *testing.T) {
// error (because only one is allowed). But the first watch still receives // error (because only one is allowed). But the first watch still receives
// updates. // updates.
func (s) TestServiceWatchSecond(t *testing.T) { func (s) TestServiceWatchSecond(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -185,17 +182,17 @@ func (s) TestServiceWatchSecond(t *testing.T) {
wantUpdate := ServiceUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}} wantUpdate := ServiceUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}}
if _, err := v2Client.addWatches[ldsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ListenerURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: {routeName: testRDSName}, testLDSName: {RouteConfigName: testRDSName},
}) })
if _, err := v2Client.addWatches[rdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2RouteConfigURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName: {routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}}, testRDSName: {Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}},
}) })
if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{wantUpdate, nil}, serviceCmpOpts...) { if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{wantUpdate, nil}, serviceCmpOpts...) {
@ -222,11 +219,11 @@ func (s) TestServiceWatchSecond(t *testing.T) {
// Send update again, first callback should be called, second should // Send update again, first callback should be called, second should
// timeout. // timeout.
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: {routeName: testRDSName}, testLDSName: {RouteConfigName: testRDSName},
}) })
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName: {routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}}, testRDSName: {Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}},
}) })
if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{wantUpdate, nil}, serviceCmpOpts...) { if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{wantUpdate, nil}, serviceCmpOpts...) {
@ -242,135 +239,130 @@ func (s) TestServiceWatchSecond(t *testing.T) {
// does not respond to the requests being sent out as part of registering a // does not respond to the requests being sent out as part of registering a
// service update watcher. The callback will get an error. // service update watcher. The callback will get an error.
func (s) TestServiceWatchWithNoResponseFromServer(t *testing.T) { func (s) TestServiceWatchWithNoResponseFromServer(t *testing.T) {
fakeServer, cleanup, err := fakeserver.StartServer()
if err != nil {
t.Fatalf("Failed to start fake xDS server: %v", err)
}
defer cleanup()
xdsClient, err := New(clientOpts(fakeServer.Address))
if err != nil {
t.Fatalf("New returned error: %v", err)
}
defer xdsClient.Close()
t.Log("Created an xdsClient...")
oldWatchExpiryTimeout := defaultWatchExpiryTimeout oldWatchExpiryTimeout := defaultWatchExpiryTimeout
defaultWatchExpiryTimeout = 500 * time.Millisecond defaultWatchExpiryTimeout = 500 * time.Millisecond
defer func() { defer func() {
defaultWatchExpiryTimeout = oldWatchExpiryTimeout defaultWatchExpiryTimeout = oldWatchExpiryTimeout
}() }()
callbackCh := testutils.NewChannel() v2ClientCh, cleanup := overrideNewAPIClient()
cancelWatch := xdsClient.WatchService(goodLDSTarget1, func(su ServiceUpdate, err error) { defer cleanup()
if su.Routes != nil {
callbackCh.Send(fmt.Errorf("got WeightedCluster: %+v, want nil", su.Routes))
return
}
if err == nil {
callbackCh.Send(errors.New("xdsClient.WatchService returned error non-nil error"))
return
}
callbackCh.Send(nil)
})
defer cancelWatch()
t.Log("Registered a watcher for service updates...")
// Wait for one request from the client, but send no reponses. c, err := New(clientOpts(testXDSServer))
if _, err := fakeServer.XDSRequestChan.Receive(); err != nil { if err != nil {
t.Fatalf("Timeout expired when expecting an LDS request") t.Fatalf("failed to create client: %v", err)
}
defer c.Close()
v2Client := <-v2ClientCh
serviceUpdateCh := testutils.NewChannel()
c.WatchService(testLDSName, func(update ServiceUpdate, err error) {
serviceUpdateCh.Send(serviceUpdateErr{u: update, err: err})
})
if _, err := v2Client.addWatches[version.V2ListenerURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err)
}
u, err := serviceUpdateCh.TimedReceive(defaultWatchExpiryTimeout * 2)
if err != nil {
t.Fatalf("failed to get serviceUpdate: %v", err)
}
uu := u.(serviceUpdateErr)
if !cmp.Equal(uu.u, ServiceUpdate{}) {
t.Errorf("unexpected serviceUpdate: %v, want %v", uu.u, ServiceUpdate{})
}
if uu.err == nil {
t.Errorf("unexpected serviceError: <nil>, want error watcher timeout")
} }
waitForNilErr(t, callbackCh)
} }
// TestServiceWatchEmptyRDS tests the case where the underlying v2Client // TestServiceWatchEmptyRDS tests the case where the underlying v2Client
// receives an empty RDS response. The callback will get an error. // receives an empty RDS response. The callback will get an error.
func (s) TestServiceWatchEmptyRDS(t *testing.T) { func (s) TestServiceWatchEmptyRDS(t *testing.T) {
fakeServer, cleanup, err := fakeserver.StartServer()
if err != nil {
t.Fatalf("Failed to start fake xDS server: %v", err)
}
defer cleanup()
xdsClient, err := New(clientOpts(fakeServer.Address))
if err != nil {
t.Fatalf("New returned error: %v", err)
}
defer xdsClient.Close()
t.Log("Created an xdsClient...")
oldWatchExpiryTimeout := defaultWatchExpiryTimeout oldWatchExpiryTimeout := defaultWatchExpiryTimeout
defaultWatchExpiryTimeout = 500 * time.Millisecond defaultWatchExpiryTimeout = 500 * time.Millisecond
defer func() { defer func() {
defaultWatchExpiryTimeout = oldWatchExpiryTimeout defaultWatchExpiryTimeout = oldWatchExpiryTimeout
}() }()
callbackCh := testutils.NewChannel() v2ClientCh, cleanup := overrideNewAPIClient()
cancelWatch := xdsClient.WatchService(goodLDSTarget1, func(su ServiceUpdate, err error) { defer cleanup()
if su.Routes != nil {
callbackCh.Send(fmt.Errorf("got WeightedCluster: %+v, want nil", su.Routes)) c, err := New(clientOpts(testXDSServer))
return if err != nil {
} t.Fatalf("failed to create client: %v", err)
if err == nil { }
callbackCh.Send(errors.New("xdsClient.WatchService returned error non-nil error")) defer c.Close()
return
} v2Client := <-v2ClientCh
callbackCh.Send(nil)
serviceUpdateCh := testutils.NewChannel()
c.WatchService(testLDSName, func(update ServiceUpdate, err error) {
serviceUpdateCh.Send(serviceUpdateErr{u: update, err: err})
}) })
defer cancelWatch()
t.Log("Registered a watcher for service updates...")
// Make the fakeServer send LDS response. if _, err := v2Client.addWatches[version.V2ListenerURL].Receive(); err != nil {
if _, err := fakeServer.XDSRequestChan.Receive(); err != nil { t.Fatalf("want new watch to start, got error %v", err)
t.Fatalf("Timeout expired when expecting an LDS request")
} }
fakeServer.XDSResponseChan <- &fakeserver.Response{Resp: goodLDSResponse1} v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: {RouteConfigName: testRDSName},
// Make the fakeServer send an empty RDS response. })
if _, err := fakeServer.XDSRequestChan.Receive(); err != nil { if _, err := v2Client.addWatches[version.V2RouteConfigURL].Receive(); err != nil {
t.Fatalf("Timeout expired when expecting an RDS request") t.Fatalf("want new watch to start, got error %v", err)
}
v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{})
u, err := serviceUpdateCh.TimedReceive(defaultWatchExpiryTimeout * 2)
if err != nil {
t.Fatalf("failed to get serviceUpdate: %v", err)
}
uu := u.(serviceUpdateErr)
if !cmp.Equal(uu.u, ServiceUpdate{}) {
t.Errorf("unexpected serviceUpdate: %v, want %v", uu.u, ServiceUpdate{})
}
if uu.err == nil {
t.Errorf("unexpected serviceError: <nil>, want error watcher timeout")
} }
fakeServer.XDSResponseChan <- &fakeserver.Response{Resp: noVirtualHostsInRDSResponse}
waitForNilErr(t, callbackCh)
} }
// TestServiceWatchWithClientClose tests the case where xDS responses are // TestServiceWatchWithClientClose tests the case where xDS responses are
// received after the client is closed, and we make sure that the registered // received after the client is closed, and we make sure that the registered
// watcher callback is not invoked. // watcher callback is not invoked.
func (s) TestServiceWatchWithClientClose(t *testing.T) { func (s) TestServiceWatchWithClientClose(t *testing.T) {
fakeServer, cleanup, err := fakeserver.StartServer() oldWatchExpiryTimeout := defaultWatchExpiryTimeout
if err != nil { defaultWatchExpiryTimeout = 500 * time.Millisecond
t.Fatalf("Failed to start fake xDS server: %v", err) defer func() {
} defaultWatchExpiryTimeout = oldWatchExpiryTimeout
}()
v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
xdsClient, err := New(clientOpts(fakeServer.Address)) c, err := New(clientOpts(testXDSServer))
if err != nil { if err != nil {
t.Fatalf("New returned error: %v", err) t.Fatalf("failed to create client: %v", err)
} }
defer xdsClient.Close() defer c.Close()
t.Log("Created an xdsClient...")
callbackCh := testutils.NewChannel() v2Client := <-v2ClientCh
cancelWatch := xdsClient.WatchService(goodLDSTarget1, func(su ServiceUpdate, err error) {
callbackCh.Send(errors.New("watcher callback invoked after client close")) serviceUpdateCh := testutils.NewChannel()
c.WatchService(testLDSName, func(update ServiceUpdate, err error) {
serviceUpdateCh.Send(serviceUpdateErr{u: update, err: err})
}) })
defer cancelWatch()
t.Log("Registered a watcher for service updates...")
// Make the fakeServer send LDS response. if _, err := v2Client.addWatches[version.V2ListenerURL].Receive(); err != nil {
if _, err := fakeServer.XDSRequestChan.Receive(); err != nil { t.Fatalf("want new watch to start, got error %v", err)
t.Fatalf("Timeout expired when expecting an LDS request")
} }
fakeServer.XDSResponseChan <- &fakeserver.Response{Resp: goodLDSResponse1} v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: {RouteConfigName: testRDSName},
xdsClient.Close() })
t.Log("Closing the xdsClient...") if _, err := v2Client.addWatches[version.V2RouteConfigURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err)
// Push an RDS response from the fakeserver }
fakeServer.XDSResponseChan <- &fakeserver.Response{Resp: goodRDSResponse1} // Client is closed before it receives the RDS response.
if cbErr, err := callbackCh.Receive(); err != testutils.ErrRecvTimeout { c.Close()
t.Fatal(cbErr) if u, err := serviceUpdateCh.TimedReceive(defaultWatchExpiryTimeout * 2); err != testutils.ErrRecvTimeout {
t.Errorf("unexpected serviceUpdate: %v, %v, want channel recv timeout", u, err)
} }
} }
@ -378,7 +370,7 @@ func (s) TestServiceWatchWithClientClose(t *testing.T) {
// update contains the same RDS name as the previous, the RDS watch isn't // update contains the same RDS name as the previous, the RDS watch isn't
// canceled and restarted. // canceled and restarted.
func (s) TestServiceNotCancelRDSOnSameLDSUpdate(t *testing.T) { func (s) TestServiceNotCancelRDSOnSameLDSUpdate(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -396,17 +388,17 @@ func (s) TestServiceNotCancelRDSOnSameLDSUpdate(t *testing.T) {
wantUpdate := ServiceUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}} wantUpdate := ServiceUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}}
if _, err := v2Client.addWatches[ldsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ListenerURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: {routeName: testRDSName}, testLDSName: {RouteConfigName: testRDSName},
}) })
if _, err := v2Client.addWatches[rdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2RouteConfigURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName: {routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}}, testRDSName: {Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}},
}) })
if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{wantUpdate, nil}, serviceCmpOpts...) { if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{wantUpdate, nil}, serviceCmpOpts...) {
@ -414,10 +406,10 @@ func (s) TestServiceNotCancelRDSOnSameLDSUpdate(t *testing.T) {
} }
// Another LDS update with a the same RDS_name. // Another LDS update with a the same RDS_name.
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: {routeName: testRDSName}, testLDSName: {RouteConfigName: testRDSName},
}) })
if v, err := v2Client.removeWatches[rdsURL].Receive(); err == nil { if v, err := v2Client.removeWatches[version.V2RouteConfigURL].Receive(); err == nil {
t.Fatalf("unexpected rds watch cancel: %v", v) t.Fatalf("unexpected rds watch cancel: %v", v)
} }
} }
@ -429,7 +421,7 @@ func (s) TestServiceNotCancelRDSOnSameLDSUpdate(t *testing.T) {
// - one more update without the removed resource // - one more update without the removed resource
// - the callback (above) shouldn't receive any update // - the callback (above) shouldn't receive any update
func (s) TestServiceResourceRemoved(t *testing.T) { func (s) TestServiceResourceRemoved(t *testing.T) {
v2ClientCh, cleanup := overrideNewXDSV2Client() v2ClientCh, cleanup := overrideNewAPIClient()
defer cleanup() defer cleanup()
c, err := New(clientOpts(testXDSServer)) c, err := New(clientOpts(testXDSServer))
@ -447,17 +439,17 @@ func (s) TestServiceResourceRemoved(t *testing.T) {
wantUpdate := ServiceUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}} wantUpdate := ServiceUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}}
if _, err := v2Client.addWatches[ldsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2ListenerURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: {routeName: testRDSName}, testLDSName: {RouteConfigName: testRDSName},
}) })
if _, err := v2Client.addWatches[rdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2RouteConfigURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName: {routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}}, testRDSName: {Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName: 1}}}},
}) })
if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{wantUpdate, nil}, serviceCmpOpts...) { if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{wantUpdate, nil}, serviceCmpOpts...) {
@ -466,8 +458,8 @@ func (s) TestServiceResourceRemoved(t *testing.T) {
// Remove LDS resource, should cancel the RDS watch, and trigger resource // Remove LDS resource, should cancel the RDS watch, and trigger resource
// removed error. // removed error.
v2Client.r.newLDSUpdate(map[string]ldsUpdate{}) v2Client.r.NewListeners(map[string]ListenerUpdate{})
if _, err := v2Client.removeWatches[rdsURL].Receive(); err != nil { if _, err := v2Client.removeWatches[version.V2RouteConfigURL].Receive(); err != nil {
t.Fatalf("want watch to be canceled, got error %v", err) t.Fatalf("want watch to be canceled, got error %v", err)
} }
if u, err := serviceUpdateCh.Receive(); err != nil || ErrType(u.(serviceUpdateErr).err) != ErrorTypeResourceNotFound { if u, err := serviceUpdateCh.Receive(); err != nil || ErrType(u.(serviceUpdateErr).err) != ErrorTypeResourceNotFound {
@ -476,8 +468,8 @@ func (s) TestServiceResourceRemoved(t *testing.T) {
// Send RDS update for the removed LDS resource, expect no updates to // Send RDS update for the removed LDS resource, expect no updates to
// callback, because RDS should be canceled. // callback, because RDS should be canceled.
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName: {routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName + "new": 1}}}}, testRDSName: {Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName + "new": 1}}}},
}) })
if u, err := serviceUpdateCh.Receive(); err != testutils.ErrRecvTimeout { if u, err := serviceUpdateCh.Receive(); err != testutils.ErrRecvTimeout {
t.Errorf("unexpected serviceUpdate: %v, want receiving from channel timeout", u) t.Errorf("unexpected serviceUpdate: %v, want receiving from channel timeout", u)
@ -486,18 +478,18 @@ func (s) TestServiceResourceRemoved(t *testing.T) {
// Add LDS resource, but not RDS resource, should // Add LDS resource, but not RDS resource, should
// - start a new RDS watch // - start a new RDS watch
// - timeout on service channel, because RDS cache was cleared // - timeout on service channel, because RDS cache was cleared
v2Client.r.newLDSUpdate(map[string]ldsUpdate{ v2Client.r.NewListeners(map[string]ListenerUpdate{
testLDSName: {routeName: testRDSName}, testLDSName: {RouteConfigName: testRDSName},
}) })
if _, err := v2Client.addWatches[rdsURL].Receive(); err != nil { if _, err := v2Client.addWatches[version.V2RouteConfigURL].Receive(); err != nil {
t.Fatalf("want new watch to start, got error %v", err) t.Fatalf("want new watch to start, got error %v", err)
} }
if u, err := serviceUpdateCh.Receive(); err != testutils.ErrRecvTimeout { if u, err := serviceUpdateCh.Receive(); err != testutils.ErrRecvTimeout {
t.Errorf("unexpected serviceUpdate: %v, want receiving from channel timeout", u) t.Errorf("unexpected serviceUpdate: %v, want receiving from channel timeout", u)
} }
v2Client.r.newRDSUpdate(map[string]rdsUpdate{ v2Client.r.NewRouteConfigs(map[string]RouteConfigUpdate{
testRDSName: {routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName + "new2": 1}}}}, testRDSName: {Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName + "new2": 1}}}},
}) })
if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{ServiceUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName + "new2": 1}}}}, nil}, serviceCmpOpts...) { if u, err := serviceUpdateCh.Receive(); err != nil || !cmp.Equal(u, serviceUpdateErr{ServiceUpdate{Routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{testCDSName + "new2": 1}}}}, nil}, serviceCmpOpts...) {
t.Errorf("unexpected serviceUpdate: %v, error receiving from channel: %v", u, err) t.Errorf("unexpected serviceUpdate: %v, error receiving from channel: %v", u, err)

View File

@ -0,0 +1,507 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package client
import (
"fmt"
"net"
"strconv"
"strings"
v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"github.com/golang/protobuf/proto"
anypb "github.com/golang/protobuf/ptypes/any"
"google.golang.org/grpc/internal/grpclog"
"google.golang.org/grpc/xds/internal"
"google.golang.org/grpc/xds/internal/version"
)
// UnmarshalListener processes resources received in an LDS response, validates
// them, and transforms them into a native struct which contains only fields we
// are interested in.
func UnmarshalListener(resources []*anypb.Any, logger *grpclog.PrefixLogger) (map[string]ListenerUpdate, error) {
update := make(map[string]ListenerUpdate)
for _, r := range resources {
if t := r.GetTypeUrl(); t != version.V2ListenerURL && t != version.V3ListenerURL {
return nil, fmt.Errorf("xds: unexpected resource type: %s in LDS response", t)
}
lis := &v3listenerpb.Listener{}
if err := proto.Unmarshal(r.GetValue(), lis); err != nil {
return nil, fmt.Errorf("xds: failed to unmarshal resource in LDS response: %v", err)
}
logger.Infof("Resource with name: %v, type: %T, contains: %v", lis.GetName(), lis, lis)
routeName, err := getRouteConfigNameFromListener(lis, logger)
if err != nil {
return nil, err
}
update[lis.GetName()] = ListenerUpdate{RouteConfigName: routeName}
}
return update, nil
}
// getRouteConfigNameFromListener checks if the provided Listener proto meets
// the expected criteria. If so, it returns a non-empty routeConfigName.
func getRouteConfigNameFromListener(lis *v3listenerpb.Listener, logger *grpclog.PrefixLogger) (string, error) {
if lis.GetApiListener() == nil {
return "", fmt.Errorf("xds: no api_listener field in LDS response %+v", lis)
}
apiLisAny := lis.GetApiListener().GetApiListener()
if t := apiLisAny.GetTypeUrl(); t != version.V3HTTPConnManagerURL && t != version.V2HTTPConnManagerURL {
return "", fmt.Errorf("xds: unexpected resource type: %s in LDS response", t)
}
apiLis := &v3httppb.HttpConnectionManager{}
if err := proto.Unmarshal(apiLisAny.GetValue(), apiLis); err != nil {
return "", fmt.Errorf("xds: failed to unmarshal api_listner in LDS response: %v", err)
}
logger.Infof("Resource with type %T, contains %v", apiLis, apiLis)
switch apiLis.RouteSpecifier.(type) {
case *v3httppb.HttpConnectionManager_Rds:
if apiLis.GetRds().GetConfigSource().GetAds() == nil {
return "", fmt.Errorf("xds: ConfigSource is not ADS in LDS response: %+v", lis)
}
name := apiLis.GetRds().GetRouteConfigName()
if name == "" {
return "", fmt.Errorf("xds: empty route_config_name in LDS response: %+v", lis)
}
return name, nil
case *v3httppb.HttpConnectionManager_RouteConfig:
// TODO: Add support for specifying the RouteConfiguration inline
// in the LDS response.
return "", fmt.Errorf("xds: LDS response contains RDS config inline. Not supported for now: %+v", apiLis)
case nil:
return "", fmt.Errorf("xds: no RouteSpecifier in received LDS response: %+v", apiLis)
default:
return "", fmt.Errorf("xds: unsupported type %T for RouteSpecifier in received LDS response", apiLis.RouteSpecifier)
}
}
// UnmarshalRouteConfig processes resources received in an RDS response,
// validates them, and transforms them into a native struct which contains only
// fields we are interested in. The provided hostname determines the route
// configuration resources of interest.
func UnmarshalRouteConfig(resources []*anypb.Any, hostname string, logger *grpclog.PrefixLogger) (map[string]RouteConfigUpdate, error) {
update := make(map[string]RouteConfigUpdate)
for _, r := range resources {
if t := r.GetTypeUrl(); t != version.V2RouteConfigURL && t != version.V3RouteConfigURL {
return nil, fmt.Errorf("xds: unexpected resource type: %s in RDS response", t)
}
rc := &v3routepb.RouteConfiguration{}
if err := proto.Unmarshal(r.GetValue(), rc); err != nil {
return nil, fmt.Errorf("xds: failed to unmarshal resource in RDS response: %v", err)
}
logger.Infof("Resource with name: %v, type: %T, contains: %v. Picking routes for current watching hostname %v", rc.GetName(), rc, rc, hostname)
// Use the hostname (resourceName for LDS) to find the routes.
u, err := generateRDSUpdateFromRouteConfiguration(rc, hostname, logger)
if err != nil {
return nil, fmt.Errorf("xds: received invalid RouteConfiguration in RDS response: %+v with err: %v", rc, err)
}
update[rc.GetName()] = u
}
return update, nil
}
// generateRDSUpdateFromRouteConfiguration checks if the provided
// RouteConfiguration meets the expected criteria. If so, it returns a
// RouteConfigUpdate with nil error.
//
// A RouteConfiguration resource is considered valid when only if it contains a
// VirtualHost whose domain field matches the server name from the URI passed
// to the gRPC channel, and it contains a clusterName or a weighted cluster.
//
// The RouteConfiguration includes a list of VirtualHosts, which may have zero
// or more elements. We are interested in the element whose domains field
// matches the server name specified in the "xds:" URI. The only field in the
// VirtualHost proto that the we are interested in is the list of routes. We
// only look at the last route in the list (the default route), whose match
// field must be empty and whose route field must be set. Inside that route
// message, the cluster field will contain the clusterName or weighted clusters
// we are looking for.
func generateRDSUpdateFromRouteConfiguration(rc *v3routepb.RouteConfiguration, host string, logger *grpclog.PrefixLogger) (RouteConfigUpdate, error) {
//
// Currently this returns "" on error, and the caller will return an error.
// But the error doesn't contain details of why the response is invalid
// (mismatch domain or empty route).
//
// For logging purposes, we can log in line. But if we want to populate
// error details for nack, a detailed error needs to be returned.
vh := findBestMatchingVirtualHost(host, rc.GetVirtualHosts())
if vh == nil {
// No matching virtual host found.
return RouteConfigUpdate{}, fmt.Errorf("no matching virtual host found")
}
if len(vh.Routes) == 0 {
// The matched virtual host has no routes, this is invalid because there
// should be at least one default route.
return RouteConfigUpdate{}, fmt.Errorf("matched virtual host has no routes")
}
routes, err := routesProtoToSlice(vh.Routes, logger)
if err != nil {
return RouteConfigUpdate{}, fmt.Errorf("received route is invalid: %v", err)
}
return RouteConfigUpdate{Routes: routes}, nil
}
func routesProtoToSlice(routes []*v3routepb.Route, logger *grpclog.PrefixLogger) ([]*Route, error) {
var routesRet []*Route
for _, r := range routes {
match := r.GetMatch()
if match == nil {
return nil, fmt.Errorf("route %+v doesn't have a match", r)
}
if len(match.GetQueryParameters()) != 0 {
// Ignore route with query parameters.
logger.Warningf("route %+v has query parameter matchers, the route will be ignored", r)
continue
}
if caseSensitive := match.GetCaseSensitive(); caseSensitive != nil && !caseSensitive.Value {
return nil, fmt.Errorf("route %+v has case-sensitive false", r)
}
pathSp := match.GetPathSpecifier()
if pathSp == nil {
return nil, fmt.Errorf("route %+v doesn't have a path specifier", r)
}
var route Route
switch pt := pathSp.(type) {
case *v3routepb.RouteMatch_Prefix:
route.Prefix = &pt.Prefix
case *v3routepb.RouteMatch_Path:
route.Path = &pt.Path
case *v3routepb.RouteMatch_SafeRegex:
route.Regex = &pt.SafeRegex.Regex
default:
logger.Warningf("route %+v has an unrecognized path specifier: %+v", r, pt)
continue
}
for _, h := range match.GetHeaders() {
var header HeaderMatcher
switch ht := h.GetHeaderMatchSpecifier().(type) {
case *v3routepb.HeaderMatcher_ExactMatch:
header.ExactMatch = &ht.ExactMatch
case *v3routepb.HeaderMatcher_SafeRegexMatch:
header.RegexMatch = &ht.SafeRegexMatch.Regex
case *v3routepb.HeaderMatcher_RangeMatch:
header.RangeMatch = &Int64Range{
Start: ht.RangeMatch.Start,
End: ht.RangeMatch.End,
}
case *v3routepb.HeaderMatcher_PresentMatch:
header.PresentMatch = &ht.PresentMatch
case *v3routepb.HeaderMatcher_PrefixMatch:
header.PrefixMatch = &ht.PrefixMatch
case *v3routepb.HeaderMatcher_SuffixMatch:
header.SuffixMatch = &ht.SuffixMatch
default:
logger.Warningf("route %+v has an unrecognized header matcher: %+v", r, ht)
continue
}
header.Name = h.GetName()
invert := h.GetInvertMatch()
header.InvertMatch = &invert
route.Headers = append(route.Headers, &header)
}
if fr := match.GetRuntimeFraction(); fr != nil {
d := fr.GetDefaultValue()
n := d.GetNumerator()
switch d.GetDenominator() {
case v3typepb.FractionalPercent_HUNDRED:
n *= 10000
case v3typepb.FractionalPercent_TEN_THOUSAND:
n *= 100
case v3typepb.FractionalPercent_MILLION:
}
route.Fraction = &n
}
clusters := make(map[string]uint32)
switch a := r.GetRoute().GetClusterSpecifier().(type) {
case *v3routepb.RouteAction_Cluster:
clusters[a.Cluster] = 1
case *v3routepb.RouteAction_WeightedClusters:
wcs := a.WeightedClusters
var totalWeight uint32
for _, c := range wcs.Clusters {
w := c.GetWeight().GetValue()
clusters[c.GetName()] = w
totalWeight += w
}
if totalWeight != wcs.GetTotalWeight().GetValue() {
return nil, fmt.Errorf("route %+v, action %+v, weights of clusters do not add up to total total weight, got: %v, want %v", r, a, wcs.GetTotalWeight().GetValue(), totalWeight)
}
case *v3routepb.RouteAction_ClusterHeader:
continue
}
route.Action = clusters
routesRet = append(routesRet, &route)
}
return routesRet, nil
}
type domainMatchType int
const (
domainMatchTypeInvalid domainMatchType = iota
domainMatchTypeUniversal
domainMatchTypePrefix
domainMatchTypeSuffix
domainMatchTypeExact
)
// Exact > Suffix > Prefix > Universal > Invalid.
func (t domainMatchType) betterThan(b domainMatchType) bool {
return t > b
}
func matchTypeForDomain(d string) domainMatchType {
if d == "" {
return domainMatchTypeInvalid
}
if d == "*" {
return domainMatchTypeUniversal
}
if strings.HasPrefix(d, "*") {
return domainMatchTypeSuffix
}
if strings.HasSuffix(d, "*") {
return domainMatchTypePrefix
}
if strings.Contains(d, "*") {
return domainMatchTypeInvalid
}
return domainMatchTypeExact
}
func match(domain, host string) (domainMatchType, bool) {
switch typ := matchTypeForDomain(domain); typ {
case domainMatchTypeInvalid:
return typ, false
case domainMatchTypeUniversal:
return typ, true
case domainMatchTypePrefix:
// abc.*
return typ, strings.HasPrefix(host, strings.TrimSuffix(domain, "*"))
case domainMatchTypeSuffix:
// *.123
return typ, strings.HasSuffix(host, strings.TrimPrefix(domain, "*"))
case domainMatchTypeExact:
return typ, domain == host
default:
return domainMatchTypeInvalid, false
}
}
// findBestMatchingVirtualHost returns the virtual host whose domains field best
// matches host
//
// The domains field support 4 different matching pattern types:
// - Exact match
// - Suffix match (e.g. “*ABC”)
// - Prefix match (e.g. “ABC*)
// - Universal match (e.g. “*”)
//
// The best match is defined as:
// - A match is better if its matching pattern type is better
// - Exact match > suffix match > prefix match > universal match
// - If two matches are of the same pattern type, the longer match is better
// - This is to compare the length of the matching pattern, e.g. “*ABCDE” >
// “*ABC”
func findBestMatchingVirtualHost(host string, vHosts []*v3routepb.VirtualHost) *v3routepb.VirtualHost {
var (
matchVh *v3routepb.VirtualHost
matchType = domainMatchTypeInvalid
matchLen int
)
for _, vh := range vHosts {
for _, domain := range vh.GetDomains() {
typ, matched := match(domain, host)
if typ == domainMatchTypeInvalid {
// The rds response is invalid.
return nil
}
if matchType.betterThan(typ) || matchType == typ && matchLen >= len(domain) || !matched {
// The previous match has better type, or the previous match has
// better length, or this domain isn't a match.
continue
}
matchVh = vh
matchType = typ
matchLen = len(domain)
}
}
return matchVh
}
// UnmarshalCluster processes resources received in an CDS response, validates
// them, and transforms them into a native struct which contains only fields we
// are interested in.
func UnmarshalCluster(resources []*anypb.Any, logger *grpclog.PrefixLogger) (map[string]ClusterUpdate, error) {
update := make(map[string]ClusterUpdate)
for _, r := range resources {
if t := r.GetTypeUrl(); t != version.V2ClusterURL && t != version.V3ClusterURL {
return nil, fmt.Errorf("xds: unexpected resource type: %s in CDS response", t)
}
cluster := &v3clusterpb.Cluster{}
if err := proto.Unmarshal(r.GetValue(), cluster); err != nil {
return nil, fmt.Errorf("xds: failed to unmarshal resource in CDS response: %v", err)
}
logger.Infof("Resource with name: %v, type: %T, contains: %v", cluster.GetName(), cluster, cluster)
cu, err := validateCluster(cluster)
if err != nil {
return nil, err
}
// If the Cluster message in the CDS response did not contain a
// serviceName, we will just use the clusterName for EDS.
if cu.ServiceName == "" {
cu.ServiceName = cluster.GetName()
}
logger.Debugf("Resource with name %v, value %+v added to cache", cluster.GetName(), cu)
update[cluster.GetName()] = cu
}
return update, nil
}
func validateCluster(cluster *v3clusterpb.Cluster) (ClusterUpdate, error) {
emptyUpdate := ClusterUpdate{ServiceName: "", EnableLRS: false}
switch {
case cluster.GetType() != v3clusterpb.Cluster_EDS:
return emptyUpdate, fmt.Errorf("xds: unexpected cluster type %v in response: %+v", cluster.GetType(), cluster)
case cluster.GetEdsClusterConfig().GetEdsConfig().GetAds() == nil:
return emptyUpdate, fmt.Errorf("xds: unexpected edsConfig in response: %+v", cluster)
case cluster.GetLbPolicy() != v3clusterpb.Cluster_ROUND_ROBIN:
return emptyUpdate, fmt.Errorf("xds: unexpected lbPolicy %v in response: %+v", cluster.GetLbPolicy(), cluster)
}
return ClusterUpdate{
ServiceName: cluster.GetEdsClusterConfig().GetServiceName(),
EnableLRS: cluster.GetLrsServer().GetSelf() != nil,
}, nil
}
// UnmarshalEndpoints processes resources received in an EDS response,
// validates them, and transforms them into a native struct which contains only
// fields we are interested in.
func UnmarshalEndpoints(resources []*anypb.Any, logger *grpclog.PrefixLogger) (map[string]EndpointsUpdate, error) {
update := make(map[string]EndpointsUpdate)
for _, r := range resources {
if t := r.GetTypeUrl(); t != version.V2EndpointsURL && t != version.V3EndpointsURL {
return nil, fmt.Errorf("xds: unexpected resource type: %s in EDS response", t)
}
cla := &v3endpointpb.ClusterLoadAssignment{}
if err := proto.Unmarshal(r.GetValue(), cla); err != nil {
return nil, fmt.Errorf("xds: failed to unmarshal resource in EDS response: %v", err)
}
logger.Infof("Resource with name: %v, type: %T, contains: %v", cla.GetClusterName(), cla, cla)
u, err := parseEDSRespProto(cla)
if err != nil {
return nil, err
}
update[cla.GetClusterName()] = u
}
return update, nil
}
func parseAddress(socketAddress *v3corepb.SocketAddress) string {
return net.JoinHostPort(socketAddress.GetAddress(), strconv.Itoa(int(socketAddress.GetPortValue())))
}
func parseDropPolicy(dropPolicy *v3endpointpb.ClusterLoadAssignment_Policy_DropOverload) OverloadDropConfig {
percentage := dropPolicy.GetDropPercentage()
var (
numerator = percentage.GetNumerator()
denominator uint32
)
switch percentage.GetDenominator() {
case v3typepb.FractionalPercent_HUNDRED:
denominator = 100
case v3typepb.FractionalPercent_TEN_THOUSAND:
denominator = 10000
case v3typepb.FractionalPercent_MILLION:
denominator = 1000000
}
return OverloadDropConfig{
Category: dropPolicy.GetCategory(),
Numerator: numerator,
Denominator: denominator,
}
}
func parseEndpoints(lbEndpoints []*v3endpointpb.LbEndpoint) []Endpoint {
endpoints := make([]Endpoint, 0, len(lbEndpoints))
for _, lbEndpoint := range lbEndpoints {
endpoints = append(endpoints, Endpoint{
HealthStatus: EndpointHealthStatus(lbEndpoint.GetHealthStatus()),
Address: parseAddress(lbEndpoint.GetEndpoint().GetAddress().GetSocketAddress()),
Weight: lbEndpoint.GetLoadBalancingWeight().GetValue(),
})
}
return endpoints
}
func parseEDSRespProto(m *v3endpointpb.ClusterLoadAssignment) (EndpointsUpdate, error) {
ret := EndpointsUpdate{}
for _, dropPolicy := range m.GetPolicy().GetDropOverloads() {
ret.Drops = append(ret.Drops, parseDropPolicy(dropPolicy))
}
priorities := make(map[uint32]struct{})
for _, locality := range m.Endpoints {
l := locality.GetLocality()
if l == nil {
return EndpointsUpdate{}, fmt.Errorf("EDS response contains a locality without ID, locality: %+v", locality)
}
lid := internal.LocalityID{
Region: l.Region,
Zone: l.Zone,
SubZone: l.SubZone,
}
priority := locality.GetPriority()
priorities[priority] = struct{}{}
ret.Localities = append(ret.Localities, Locality{
ID: lid,
Endpoints: parseEndpoints(locality.GetLbEndpoints()),
Weight: locality.GetLoadBalancingWeight().GetValue(),
Priority: priority,
})
}
for i := 0; i < len(priorities); i++ {
if _, ok := priorities[uint32(i)]; !ok {
return EndpointsUpdate{}, fmt.Errorf("priority %v missing (with different priorities %v received)", i, priorities)
}
}
return ret, nil
}

View File

@ -0,0 +1,122 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package tests
import (
"testing"
"google.golang.org/grpc"
"google.golang.org/grpc/internal/grpctest"
xdsclient "google.golang.org/grpc/xds/internal/client"
"google.golang.org/grpc/xds/internal/client/bootstrap"
_ "google.golang.org/grpc/xds/internal/client/v2" // Register the v2 API client.
"google.golang.org/grpc/xds/internal/testutils"
"google.golang.org/grpc/xds/internal/version"
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
const (
testXDSServer = "xds-server"
)
func clientOpts(balancerName string) xdsclient.Options {
return xdsclient.Options{
Config: bootstrap.Config{
BalancerName: balancerName,
Creds: grpc.WithInsecure(),
NodeProto: testutils.EmptyNodeProtoV2,
},
}
}
func (s) TestNew(t *testing.T) {
tests := []struct {
name string
opts xdsclient.Options
wantErr bool
}{
{name: "empty-opts", opts: xdsclient.Options{}, wantErr: true},
{
name: "empty-balancer-name",
opts: xdsclient.Options{
Config: bootstrap.Config{
Creds: grpc.WithInsecure(),
NodeProto: testutils.EmptyNodeProtoV2,
},
},
wantErr: true,
},
{
name: "empty-dial-creds",
opts: xdsclient.Options{
Config: bootstrap.Config{
BalancerName: testXDSServer,
NodeProto: testutils.EmptyNodeProtoV2,
},
},
wantErr: true,
},
{
name: "empty-node-proto",
opts: xdsclient.Options{
Config: bootstrap.Config{
BalancerName: testXDSServer,
Creds: grpc.WithInsecure(),
},
},
wantErr: true,
},
{
name: "node-proto-version-mismatch",
opts: xdsclient.Options{
Config: bootstrap.Config{
BalancerName: testXDSServer,
Creds: grpc.WithInsecure(),
NodeProto: testutils.EmptyNodeProtoV3,
TransportAPI: version.TransportV2,
},
},
wantErr: true,
},
// TODO(easwars): Add cases for v3 API client.
{
name: "happy-case",
opts: clientOpts(testXDSServer),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c, err := xdsclient.New(test.opts)
if (err != nil) != test.wantErr {
t.Fatalf("New(%+v) = %v, wantErr: %v", test.opts, err, test.wantErr)
}
if c != nil {
c.Close()
}
})
}
}

View File

@ -1,209 +0,0 @@
/*
*
* Copyright 2019 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client
import (
"reflect"
"testing"
"time"
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc"
"google.golang.org/grpc/xds/internal/testutils"
"google.golang.org/grpc/xds/internal/testutils/fakeserver"
)
type watchHandleTestcase struct {
typeURL string
resourceName string
responseToHandle *xdspb.DiscoveryResponse
wantHandleErr bool
wantUpdate interface{}
wantUpdateErr bool
}
type testUpdateReceiver struct {
f func(typeURL string, d map[string]interface{})
}
func (t *testUpdateReceiver) newLDSUpdate(d map[string]ldsUpdate) {
dd := make(map[string]interface{})
for k, v := range d {
dd[k] = v
}
t.newUpdate(ldsURL, dd)
}
func (t *testUpdateReceiver) newRDSUpdate(d map[string]rdsUpdate) {
dd := make(map[string]interface{})
for k, v := range d {
dd[k] = v
}
t.newUpdate(rdsURL, dd)
}
func (t *testUpdateReceiver) newCDSUpdate(d map[string]ClusterUpdate) {
dd := make(map[string]interface{})
for k, v := range d {
dd[k] = v
}
t.newUpdate(cdsURL, dd)
}
func (t *testUpdateReceiver) newEDSUpdate(d map[string]EndpointsUpdate) {
dd := make(map[string]interface{})
for k, v := range d {
dd[k] = v
}
t.newUpdate(edsURL, dd)
}
func (t *testUpdateReceiver) newUpdate(typeURL string, d map[string]interface{}) {
t.f(typeURL, d)
}
// testWatchHandle is called to test response handling for each xDS.
//
// It starts the xDS watch as configured in test, waits for the fake xds server
// to receive the request (so watch callback is installed), and calls
// handleXDSResp with responseToHandle (if it's set). It then compares the
// update received by watch callback with the expected results.
func testWatchHandle(t *testing.T, test *watchHandleTestcase) {
fakeServer, cc, cleanup := startServerAndGetCC(t)
defer cleanup()
type updateErr struct {
u interface{}
err error
}
gotUpdateCh := testutils.NewChannel()
v2c := newV2Client(&testUpdateReceiver{
f: func(typeURL string, d map[string]interface{}) {
if typeURL == test.typeURL {
if u, ok := d[test.resourceName]; ok {
gotUpdateCh.Send(updateErr{u, nil})
}
}
},
}, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil)
defer v2c.close()
// RDS needs an existin LDS watch for the hostname.
if test.typeURL == rdsURL {
doLDS(t, v2c, fakeServer)
}
// Register the watcher, this will also trigger the v2Client to send the xDS
// request.
v2c.addWatch(test.typeURL, test.resourceName)
// Wait till the request makes it to the fakeServer. This ensures that
// the watch request has been processed by the v2Client.
if _, err := fakeServer.XDSRequestChan.Receive(); err != nil {
t.Fatalf("Timeout waiting for an xDS request: %v", err)
}
// Directly push the response through a call to handleXDSResp. This bypasses
// the fakeServer, so it's only testing the handle logic. Client response
// processing is covered elsewhere.
//
// Also note that this won't trigger ACK, so there's no need to clear the
// request channel afterwards.
var handleXDSResp func(response *xdspb.DiscoveryResponse) error
switch test.typeURL {
case ldsURL:
handleXDSResp = v2c.handleLDSResponse
case rdsURL:
handleXDSResp = v2c.handleRDSResponse
case cdsURL:
handleXDSResp = v2c.handleCDSResponse
case edsURL:
handleXDSResp = v2c.handleEDSResponse
}
if err := handleXDSResp(test.responseToHandle); (err != nil) != test.wantHandleErr {
t.Fatalf("v2c.handleRDSResponse() returned err: %v, wantErr: %v", err, test.wantHandleErr)
}
// If the test doesn't expect the callback to be invoked, verify that no
// update or error is pushed to the callback.
//
// Cannot directly compare test.wantUpdate with nil (typed vs non-typed nil:
// https://golang.org/doc/faq#nil_error).
if c := test.wantUpdate; c == nil || (reflect.ValueOf(c).Kind() == reflect.Ptr && reflect.ValueOf(c).IsNil()) {
update, err := gotUpdateCh.Receive()
if err == testutils.ErrRecvTimeout {
return
}
t.Fatalf("Unexpected update: +%v", update)
}
wantUpdate := reflect.ValueOf(test.wantUpdate).Elem().Interface()
uErr, err := gotUpdateCh.Receive()
if err == testutils.ErrRecvTimeout {
t.Fatal("Timeout expecting xDS update")
}
gotUpdate := uErr.(updateErr).u
opt := cmp.AllowUnexported(rdsUpdate{}, ldsUpdate{}, ClusterUpdate{}, EndpointsUpdate{})
if diff := cmp.Diff(gotUpdate, wantUpdate, opt); diff != "" {
t.Fatalf("got update : %+v, want %+v, diff: %s", gotUpdate, wantUpdate, diff)
}
gotUpdateErr := uErr.(updateErr).err
if (gotUpdateErr != nil) != test.wantUpdateErr {
t.Fatalf("got xDS update error {%v}, wantErr: %v", gotUpdateErr, test.wantUpdateErr)
}
}
// startServerAndGetCC starts a fake XDS server and also returns a ClientConn
// connected to it.
func startServerAndGetCC(t *testing.T) (*fakeserver.Server, *grpc.ClientConn, func()) {
t.Helper()
fs, sCleanup, err := fakeserver.StartServer()
if err != nil {
t.Fatalf("Failed to start fake xDS server: %v", err)
}
cc, ccCleanup, err := fs.XDSClientConn()
if err != nil {
sCleanup()
t.Fatalf("Failed to get a clientConn to the fake xDS server: %v", err)
}
return fs, cc, func() {
sCleanup()
ccCleanup()
}
}
// waitForNilErr waits for a nil error value to be received on the
// provided channel.
func waitForNilErr(t *testing.T, ch *testutils.Channel) {
t.Helper()
val, err := ch.Receive()
if err == testutils.ErrRecvTimeout {
t.Fatalf("Timeout expired when expecting update")
}
if val != nil {
if cbErr := val.(error); cbErr != nil {
t.Fatal(cbErr)
}
}
}

View File

@ -16,38 +16,68 @@
* *
*/ */
package client // Package v2 provides xDS v2 transport protocol specific functionality.
package v2
import ( import (
"context" "context"
"fmt"
"sync" "sync"
"time" "time"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/internal/buffer" "google.golang.org/grpc/internal/buffer"
"google.golang.org/grpc/internal/grpclog" "google.golang.org/grpc/internal/grpclog"
xdsclient "google.golang.org/grpc/xds/internal/client"
"google.golang.org/grpc/xds/internal/version"
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2" v2xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" v2corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
adsgrpc "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v2" v2adsgrpc "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v2"
) )
type adsStream adsgrpc.AggregatedDiscoveryService_StreamAggregatedResourcesClient func init() {
xdsclient.RegisterAPIClientBuilder(clientBuilder{})
var _ xdsv2Client = &v2Client{}
// updateHandler handles the update (parsed from xds responses). It's
// implemented by the upper level Client.
//
// It's an interface to be overridden in test.
type updateHandler interface {
newLDSUpdate(d map[string]ldsUpdate)
newRDSUpdate(d map[string]rdsUpdate)
newCDSUpdate(d map[string]ClusterUpdate)
newEDSUpdate(d map[string]EndpointsUpdate)
} }
// v2Client performs the actual xDS RPCs using the xDS v2 API. It creates a type clientBuilder struct{}
func (clientBuilder) Build(cc *grpc.ClientConn, opts xdsclient.BuildOptions) (xdsclient.APIClient, error) {
return newClient(cc, opts)
}
func (clientBuilder) Version() version.TransportAPI {
return version.TransportV2
}
func newClient(cc *grpc.ClientConn, opts xdsclient.BuildOptions) (xdsclient.APIClient, error) {
nodeProto, ok := opts.NodeProto.(*v2corepb.Node)
if !ok {
return nil, fmt.Errorf("xds: unsupported Node proto type: %T, want %T", opts.NodeProto, v2corepb.Node{})
}
v2c := &client{
cc: cc,
parent: opts.Parent,
nodeProto: nodeProto,
backoff: opts.Backoff,
logger: opts.Logger,
streamCh: make(chan adsStream, 1),
sendCh: buffer.NewUnbounded(),
watchMap: make(map[string]map[string]bool),
versionMap: make(map[string]string),
nonceMap: make(map[string]string),
}
v2c.ctx, v2c.cancelCtx = context.WithCancel(context.Background())
go v2c.run()
return v2c, nil
}
type adsStream v2adsgrpc.AggregatedDiscoveryService_StreamAggregatedResourcesClient
// client performs the actual xDS RPCs using the xDS v2 API. It creates a
// single ADS stream on which the different types of xDS requests and responses // single ADS stream on which the different types of xDS requests and responses
// are multiplexed. // are multiplexed.
// //
@ -55,18 +85,14 @@ type updateHandler interface {
// and do ACK/NACK. It's a naive implementation that sends whatever the upper // and do ACK/NACK. It's a naive implementation that sends whatever the upper
// layer tells it to send. It will call the callback with everything in every // layer tells it to send. It will call the callback with everything in every
// response. It doesn't keep a cache of responses, or check for duplicates. // response. It doesn't keep a cache of responses, or check for duplicates.
// type client struct {
// The reason for splitting this out from the top level xdsClient object is
// because there is already an xDS v3Aplha API in development. If and when we
// want to switch to that, this separation will ease that process.
type v2Client struct {
ctx context.Context ctx context.Context
cancelCtx context.CancelFunc cancelCtx context.CancelFunc
parent updateHandler parent xdsclient.UpdateHandler
// ClientConn to the xDS gRPC server. Owned by the parent xdsClient. // ClientConn to the xDS gRPC server. Owned by the parent xdsClient.
cc *grpc.ClientConn cc *grpc.ClientConn
nodeProto *corepb.Node nodeProto *v2corepb.Node
backoff func(int) time.Duration backoff func(int) time.Duration
logger *grpclog.PrefixLogger logger *grpclog.PrefixLogger
@ -102,38 +128,31 @@ type v2Client struct {
hostname string hostname string
} }
// newV2Client creates a new v2Client initialized with the passed arguments. func (v2c *client) AddWatch(resourceType, resourceName string) {
func newV2Client(parent updateHandler, cc *grpc.ClientConn, nodeProto *corepb.Node, backoff func(int) time.Duration, logger *grpclog.PrefixLogger) *v2Client { v2c.sendCh.Put(&watchAction{
v2c := &v2Client{ typeURL: resourceType,
cc: cc, remove: false,
parent: parent, resource: resourceName,
nodeProto: nodeProto, })
backoff: backoff, }
logger: logger, func (v2c *client) RemoveWatch(resourceType, resourceName string) {
v2c.sendCh.Put(&watchAction{
streamCh: make(chan adsStream, 1), typeURL: resourceType,
sendCh: buffer.NewUnbounded(), remove: true,
resource: resourceName,
watchMap: make(map[string]map[string]bool), })
versionMap: make(map[string]string),
nonceMap: make(map[string]string),
}
v2c.ctx, v2c.cancelCtx = context.WithCancel(context.Background())
go v2c.run()
return v2c
} }
// close cleans up resources and goroutines allocated by this client. // close cleans up resources and goroutines allocated by this client.
func (v2c *v2Client) close() { func (v2c *client) Close() {
v2c.cancelCtx() v2c.cancelCtx()
} }
// run starts an ADS stream (and backs off exponentially, if the previous // run starts an ADS stream (and backs off exponentially, if the previous
// stream failed without receiving a single reply) and runs the sender and // stream failed without receiving a single reply) and runs the sender and
// receiver routines to send and receive data from the stream respectively. // receiver routines to send and receive data from the stream respectively.
func (v2c *v2Client) run() { func (v2c *client) run() {
go v2c.send() go v2c.send()
// TODO: start a goroutine monitoring ClientConn's connectivity state, and // TODO: start a goroutine monitoring ClientConn's connectivity state, and
// report error (and log) when stats is transient failure. // report error (and log) when stats is transient failure.
@ -159,7 +178,7 @@ func (v2c *v2Client) run() {
} }
retries++ retries++
cli := adsgrpc.NewAggregatedDiscoveryServiceClient(v2c.cc) cli := v2adsgrpc.NewAggregatedDiscoveryServiceClient(v2c.cc)
stream, err := cli.StreamAggregatedResources(v2c.ctx, grpc.WaitForReady(true)) stream, err := cli.StreamAggregatedResources(v2c.ctx, grpc.WaitForReady(true))
if err != nil { if err != nil {
v2c.logger.Warningf("xds: ADS stream creation failed: %v", err) v2c.logger.Warningf("xds: ADS stream creation failed: %v", err)
@ -187,8 +206,8 @@ func (v2c *v2Client) run() {
// - If this is an ack, version will be the version from the response // - If this is an ack, version will be the version from the response
// - If this is a nack, version will be the previous acked version (from // - If this is a nack, version will be the previous acked version (from
// versionMap). If there was no ack before, it will be an empty string // versionMap). If there was no ack before, it will be an empty string
func (v2c *v2Client) sendRequest(stream adsStream, resourceNames []string, typeURL, version, nonce string) bool { func (v2c *client) sendRequest(stream adsStream, resourceNames []string, typeURL, version, nonce string) bool {
req := &xdspb.DiscoveryRequest{ req := &v2xdspb.DiscoveryRequest{
Node: v2c.nodeProto, Node: v2c.nodeProto,
TypeUrl: typeURL, TypeUrl: typeURL,
ResourceNames: resourceNames, ResourceNames: resourceNames,
@ -210,7 +229,7 @@ func (v2c *v2Client) sendRequest(stream adsStream, resourceNames []string, typeU
// that here because the stream has just started and Send() usually returns // that here because the stream has just started and Send() usually returns
// quickly (once it pushes the message onto the transport layer) and is only // quickly (once it pushes the message onto the transport layer) and is only
// ever blocked if we don't have enough flow control quota. // ever blocked if we don't have enough flow control quota.
func (v2c *v2Client) sendExisting(stream adsStream) bool { func (v2c *client) sendExisting(stream adsStream) bool {
v2c.mu.Lock() v2c.mu.Lock()
defer v2c.mu.Unlock() defer v2c.mu.Unlock()
@ -236,7 +255,7 @@ type watchAction struct {
// processWatchInfo pulls the fields needed by the request from a watchAction. // processWatchInfo pulls the fields needed by the request from a watchAction.
// //
// It also updates the watch map in v2c. // It also updates the watch map in v2c.
func (v2c *v2Client) processWatchInfo(t *watchAction) (target []string, typeURL, version, nonce string, send bool) { func (v2c *client) processWatchInfo(t *watchAction) (target []string, typeURL, ver, nonce string, send bool) {
v2c.mu.Lock() v2c.mu.Lock()
defer v2c.mu.Unlock() defer v2c.mu.Unlock()
@ -258,7 +277,7 @@ func (v2c *v2Client) processWatchInfo(t *watchAction) (target []string, typeURL,
// Special handling for LDS, because RDS needs the LDS resource_name for // Special handling for LDS, because RDS needs the LDS resource_name for
// response host matching. // response host matching.
if t.typeURL == ldsURL { if t.typeURL == version.V2ListenerURL {
// Set hostname to the first LDS resource_name, and reset it when the // Set hostname to the first LDS resource_name, and reset it when the
// last LDS watch is removed. The upper level Client isn't expected to // last LDS watch is removed. The upper level Client isn't expected to
// watchLDS more than once. // watchLDS more than once.
@ -275,9 +294,9 @@ func (v2c *v2Client) processWatchInfo(t *watchAction) (target []string, typeURL,
// We don't reset version or nonce when a new watch is started. The version // We don't reset version or nonce when a new watch is started. The version
// and nonce from previous response are carried by the request unless the // and nonce from previous response are carried by the request unless the
// stream is recreated. // stream is recreated.
version = v2c.versionMap[typeURL] ver = v2c.versionMap[typeURL]
nonce = v2c.nonceMap[typeURL] nonce = v2c.nonceMap[typeURL]
return target, typeURL, version, nonce, send return target, typeURL, ver, nonce, send
} }
type ackAction struct { type ackAction struct {
@ -293,7 +312,7 @@ type ackAction struct {
// processAckInfo pulls the fields needed by the ack request from a ackAction. // processAckInfo pulls the fields needed by the ack request from a ackAction.
// //
// If no active watch is found for this ack, it returns false for send. // If no active watch is found for this ack, it returns false for send.
func (v2c *v2Client) processAckInfo(t *ackAction, stream adsStream) (target []string, typeURL, version, nonce string, send bool) { func (v2c *client) processAckInfo(t *ackAction, stream adsStream) (target []string, typeURL, version, nonce string, send bool) {
if t.stream != stream { if t.stream != stream {
// If ACK's stream isn't the current sending stream, this means the ACK // If ACK's stream isn't the current sending stream, this means the ACK
// was pushed to queue before the old stream broke, and a new stream has // was pushed to queue before the old stream broke, and a new stream has
@ -353,7 +372,7 @@ func (v2c *v2Client) processAckInfo(t *ackAction, stream adsStream) (target []st
// Note that this goroutine doesn't do anything to the old stream when there's a // Note that this goroutine doesn't do anything to the old stream when there's a
// new one. In fact, there should be only one stream in progress, and new one // new one. In fact, there should be only one stream in progress, and new one
// should only be created when the old one fails (recv returns an error). // should only be created when the old one fails (recv returns an error).
func (v2c *v2Client) send() { func (v2c *client) send() {
var stream adsStream var stream adsStream
for { for {
select { select {
@ -398,7 +417,7 @@ func (v2c *v2Client) send() {
// recv receives xDS responses on the provided ADS stream and branches out to // recv receives xDS responses on the provided ADS stream and branches out to
// message specific handlers. // message specific handlers.
func (v2c *v2Client) recv(stream adsStream) bool { func (v2c *client) recv(stream adsStream) bool {
success := false success := false
for { for {
resp, err := stream.Recv() resp, err := stream.Recv()
@ -409,15 +428,20 @@ func (v2c *v2Client) recv(stream adsStream) bool {
} }
v2c.logger.Infof("ADS response received, type: %v", resp.GetTypeUrl()) v2c.logger.Infof("ADS response received, type: %v", resp.GetTypeUrl())
v2c.logger.Debugf("ADS response received: %v", resp) v2c.logger.Debugf("ADS response received: %v", resp)
// Note that the xDS transport protocol is versioned independently of
// the resource types, and it is supported to transfer older versions
// of resource types using new versions of the transport protocol, or
// vice-versa. Hence we need to handle v3 type_urls as well here.
var respHandleErr error var respHandleErr error
switch resp.GetTypeUrl() { switch resp.GetTypeUrl() {
case ldsURL: case version.V2ListenerURL, version.V3ListenerURL:
respHandleErr = v2c.handleLDSResponse(resp) respHandleErr = v2c.handleLDSResponse(resp)
case rdsURL: case version.V2RouteConfigURL, version.V3RouteConfigURL:
respHandleErr = v2c.handleRDSResponse(resp) respHandleErr = v2c.handleRDSResponse(resp)
case cdsURL: case version.V2ClusterURL, version.V3ClusterURL:
respHandleErr = v2c.handleCDSResponse(resp) respHandleErr = v2c.handleCDSResponse(resp)
case edsURL: case version.V2EndpointsURL, version.V3EndpointsURL:
respHandleErr = v2c.handleEDSResponse(resp) respHandleErr = v2c.handleEDSResponse(resp)
default: default:
v2c.logger.Warningf("Resource type %v unknown in response from server", resp.GetTypeUrl()) v2c.logger.Warningf("Resource type %v unknown in response from server", resp.GetTypeUrl())
@ -446,25 +470,56 @@ func (v2c *v2Client) recv(stream adsStream) bool {
} }
} }
func (v2c *v2Client) addWatch(resourceType, resourceName string) {
v2c.sendCh.Put(&watchAction{
typeURL: resourceType,
remove: false,
resource: resourceName,
})
}
func (v2c *v2Client) removeWatch(resourceType, resourceName string) {
v2c.sendCh.Put(&watchAction{
typeURL: resourceType,
remove: true,
resource: resourceName,
})
}
func mapToSlice(m map[string]bool) (ret []string) { func mapToSlice(m map[string]bool) (ret []string) {
for i := range m { for i := range m {
ret = append(ret, i) ret = append(ret, i)
} }
return return
} }
// handleLDSResponse processes an LDS response received from the xDS server. On
// receipt of a good response, it also invokes the registered watcher callback.
func (v2c *client) handleLDSResponse(resp *v2xdspb.DiscoveryResponse) error {
update, err := xdsclient.UnmarshalListener(resp.GetResources(), v2c.logger)
if err != nil {
return err
}
v2c.parent.NewListeners(update)
return nil
}
// handleRDSResponse processes an RDS response received from the xDS server. On
// receipt of a good response, it caches validated resources and also invokes
// the registered watcher callback.
func (v2c *client) handleRDSResponse(resp *v2xdspb.DiscoveryResponse) error {
v2c.mu.Lock()
hostname := v2c.hostname
v2c.mu.Unlock()
update, err := xdsclient.UnmarshalRouteConfig(resp.GetResources(), hostname, v2c.logger)
if err != nil {
return err
}
v2c.parent.NewRouteConfigs(update)
return nil
}
// handleCDSResponse processes an CDS response received from the xDS server. On
// receipt of a good response, it also invokes the registered watcher callback.
func (v2c *client) handleCDSResponse(resp *v2xdspb.DiscoveryResponse) error {
update, err := xdsclient.UnmarshalCluster(resp.GetResources(), v2c.logger)
if err != nil {
return err
}
v2c.parent.NewClusters(update)
return nil
}
func (v2c *client) handleEDSResponse(resp *v2xdspb.DiscoveryResponse) error {
update, err := xdsclient.UnmarshalEndpoints(resp.GetResources(), v2c.logger)
if err != nil {
return err
}
v2c.parent.NewEndpoints(update)
return nil
}

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
package client package v2
import ( import (
"fmt" "fmt"
@ -30,42 +30,46 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/xds/internal/testutils" "google.golang.org/grpc/xds/internal/testutils"
"google.golang.org/grpc/xds/internal/testutils/fakeserver" "google.golang.org/grpc/xds/internal/testutils/fakeserver"
"google.golang.org/grpc/xds/internal/version"
) )
func startXDSV2Client(t *testing.T, cc *grpc.ClientConn) (v2c *v2Client, cbLDS, cbRDS, cbCDS, cbEDS *testutils.Channel, cleanup func()) { func startXDSV2Client(t *testing.T, cc *grpc.ClientConn) (v2c *client, cbLDS, cbRDS, cbCDS, cbEDS *testutils.Channel, cleanup func()) {
cbLDS = testutils.NewChannel() cbLDS = testutils.NewChannel()
cbRDS = testutils.NewChannel() cbRDS = testutils.NewChannel()
cbCDS = testutils.NewChannel() cbCDS = testutils.NewChannel()
cbEDS = testutils.NewChannel() cbEDS = testutils.NewChannel()
v2c = newV2Client(&testUpdateReceiver{ v2c, err := newV2Client(&testUpdateReceiver{
f: func(typeURL string, d map[string]interface{}) { f: func(typeURL string, d map[string]interface{}) {
t.Logf("Received %s callback with {%+v}", typeURL, d) t.Logf("Received %s callback with {%+v}", typeURL, d)
switch typeURL { switch typeURL {
case ldsURL: case version.V2ListenerURL:
if _, ok := d[goodLDSTarget1]; ok { if _, ok := d[goodLDSTarget1]; ok {
cbLDS.Send(struct{}{}) cbLDS.Send(struct{}{})
} }
case rdsURL: case version.V2RouteConfigURL:
if _, ok := d[goodRouteName1]; ok { if _, ok := d[goodRouteName1]; ok {
cbRDS.Send(struct{}{}) cbRDS.Send(struct{}{})
} }
case cdsURL: case version.V2ClusterURL:
if _, ok := d[goodClusterName1]; ok { if _, ok := d[goodClusterName1]; ok {
cbCDS.Send(struct{}{}) cbCDS.Send(struct{}{})
} }
case edsURL: case version.V2EndpointsURL:
if _, ok := d[goodEDSName]; ok { if _, ok := d[goodEDSName]; ok {
cbEDS.Send(struct{}{}) cbEDS.Send(struct{}{})
} }
} }
}, },
}, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil) }, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil)
t.Log("Started xds v2Client...") if err != nil {
return v2c, cbLDS, cbRDS, cbCDS, cbEDS, v2c.close t.Fatal(err)
}
t.Log("Started xds client...")
return v2c, cbLDS, cbRDS, cbCDS, cbEDS, v2c.Close
} }
// compareXDSRequest reads requests from channel, compare it with want. // compareXDSRequest reads requests from channel, compare it with want.
func compareXDSRequest(ch *testutils.Channel, want *xdspb.DiscoveryRequest, version, nonce string) error { func compareXDSRequest(ch *testutils.Channel, want *xdspb.DiscoveryRequest, ver, nonce string) error {
val, err := ch.Receive() val, err := ch.Receive()
if err != nil { if err != nil {
return err return err
@ -75,7 +79,7 @@ func compareXDSRequest(ch *testutils.Channel, want *xdspb.DiscoveryRequest, vers
return fmt.Errorf("unexpected error from request: %v", req.Err) return fmt.Errorf("unexpected error from request: %v", req.Err)
} }
wantClone := proto.Clone(want).(*xdspb.DiscoveryRequest) wantClone := proto.Clone(want).(*xdspb.DiscoveryRequest)
wantClone.VersionInfo = version wantClone.VersionInfo = ver
wantClone.ResponseNonce = nonce wantClone.ResponseNonce = nonce
if !cmp.Equal(req.Req, wantClone, cmp.Comparer(proto.Equal)) { if !cmp.Equal(req.Req, wantClone, cmp.Comparer(proto.Equal)) {
return fmt.Errorf("received request different from want, diff: %s", cmp.Diff(req.Req, wantClone)) return fmt.Errorf("received request different from want, diff: %s", cmp.Diff(req.Req, wantClone))
@ -83,9 +87,9 @@ func compareXDSRequest(ch *testutils.Channel, want *xdspb.DiscoveryRequest, vers
return nil return nil
} }
func sendXDSRespWithVersion(ch chan<- *fakeserver.Response, respWithoutVersion *xdspb.DiscoveryResponse, version int) (nonce string) { func sendXDSRespWithVersion(ch chan<- *fakeserver.Response, respWithoutVersion *xdspb.DiscoveryResponse, ver int) (nonce string) {
respToSend := proto.Clone(respWithoutVersion).(*xdspb.DiscoveryResponse) respToSend := proto.Clone(respWithoutVersion).(*xdspb.DiscoveryResponse)
respToSend.VersionInfo = strconv.Itoa(version) respToSend.VersionInfo = strconv.Itoa(ver)
nonce = strconv.Itoa(int(time.Now().UnixNano())) nonce = strconv.Itoa(int(time.Now().UnixNano()))
respToSend.Nonce = nonce respToSend.Nonce = nonce
ch <- &fakeserver.Response{Resp: respToSend} ch <- &fakeserver.Response{Resp: respToSend}
@ -94,25 +98,25 @@ func sendXDSRespWithVersion(ch chan<- *fakeserver.Response, respWithoutVersion *
// startXDS calls watch to send the first request. It then sends a good response // startXDS calls watch to send the first request. It then sends a good response
// and checks for ack. // and checks for ack.
func startXDS(t *testing.T, xdsname string, v2c *v2Client, reqChan *testutils.Channel, req *xdspb.DiscoveryRequest, preVersion string, preNonce string) { func startXDS(t *testing.T, xdsname string, v2c *client, reqChan *testutils.Channel, req *xdspb.DiscoveryRequest, preVersion string, preNonce string) {
var ( var (
nameToWatch, typeURLToWatch string nameToWatch, typeURLToWatch string
) )
switch xdsname { switch xdsname {
case "LDS": case "LDS":
typeURLToWatch = ldsURL typeURLToWatch = version.V2ListenerURL
nameToWatch = goodLDSTarget1 nameToWatch = goodLDSTarget1
case "RDS": case "RDS":
typeURLToWatch = rdsURL typeURLToWatch = version.V2RouteConfigURL
nameToWatch = goodRouteName1 nameToWatch = goodRouteName1
case "CDS": case "CDS":
typeURLToWatch = cdsURL typeURLToWatch = version.V2ClusterURL
nameToWatch = goodClusterName1 nameToWatch = goodClusterName1
case "EDS": case "EDS":
typeURLToWatch = edsURL typeURLToWatch = version.V2EndpointsURL
nameToWatch = goodEDSName nameToWatch = goodEDSName
} }
v2c.addWatch(typeURLToWatch, nameToWatch) v2c.AddWatch(typeURLToWatch, nameToWatch)
if err := compareXDSRequest(reqChan, req, preVersion, preNonce); err != nil { if err := compareXDSRequest(reqChan, req, preVersion, preNonce); err != nil {
t.Fatalf("Failed to receive %s request: %v", xdsname, err) t.Fatalf("Failed to receive %s request: %v", xdsname, err)
@ -125,11 +129,11 @@ func startXDS(t *testing.T, xdsname string, v2c *v2Client, reqChan *testutils.Ch
// //
// It also waits and checks that the ack request contains the given version, and // It also waits and checks that the ack request contains the given version, and
// the generated nonce. // the generated nonce.
func sendGoodResp(t *testing.T, xdsname string, fakeServer *fakeserver.Server, version int, goodResp *xdspb.DiscoveryResponse, wantReq *xdspb.DiscoveryRequest, callbackCh *testutils.Channel) (string, error) { func sendGoodResp(t *testing.T, xdsname string, fakeServer *fakeserver.Server, ver int, goodResp *xdspb.DiscoveryResponse, wantReq *xdspb.DiscoveryRequest, callbackCh *testutils.Channel) (string, error) {
nonce := sendXDSRespWithVersion(fakeServer.XDSResponseChan, goodResp, version) nonce := sendXDSRespWithVersion(fakeServer.XDSResponseChan, goodResp, ver)
t.Logf("Good %s response pushed to fakeServer...", xdsname) t.Logf("Good %s response pushed to fakeServer...", xdsname)
if err := compareXDSRequest(fakeServer.XDSRequestChan, wantReq, strconv.Itoa(version), nonce); err != nil { if err := compareXDSRequest(fakeServer.XDSRequestChan, wantReq, strconv.Itoa(ver), nonce); err != nil {
return "", fmt.Errorf("failed to receive %s request: %v", xdsname, err) return "", fmt.Errorf("failed to receive %s request: %v", xdsname, err)
} }
t.Logf("Good %s response acked", xdsname) t.Logf("Good %s response acked", xdsname)
@ -145,24 +149,24 @@ func sendGoodResp(t *testing.T, xdsname string, fakeServer *fakeserver.Server, v
// be nacked, so we expect a request with the previous version (version-1). // be nacked, so we expect a request with the previous version (version-1).
// //
// But the nonce in request should be the new nonce. // But the nonce in request should be the new nonce.
func sendBadResp(t *testing.T, xdsname string, fakeServer *fakeserver.Server, version int, wantReq *xdspb.DiscoveryRequest) error { func sendBadResp(t *testing.T, xdsname string, fakeServer *fakeserver.Server, ver int, wantReq *xdspb.DiscoveryRequest) error {
var typeURL string var typeURL string
switch xdsname { switch xdsname {
case "LDS": case "LDS":
typeURL = ldsURL typeURL = version.V2ListenerURL
case "RDS": case "RDS":
typeURL = rdsURL typeURL = version.V2RouteConfigURL
case "CDS": case "CDS":
typeURL = cdsURL typeURL = version.V2ClusterURL
case "EDS": case "EDS":
typeURL = edsURL typeURL = version.V2EndpointsURL
} }
nonce := sendXDSRespWithVersion(fakeServer.XDSResponseChan, &xdspb.DiscoveryResponse{ nonce := sendXDSRespWithVersion(fakeServer.XDSResponseChan, &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{{}}, Resources: []*anypb.Any{{}},
TypeUrl: typeURL, TypeUrl: typeURL,
}, version) }, ver)
t.Logf("Bad %s response pushed to fakeServer...", xdsname) t.Logf("Bad %s response pushed to fakeServer...", xdsname)
if err := compareXDSRequest(fakeServer.XDSRequestChan, wantReq, strconv.Itoa(version-1), nonce); err != nil { if err := compareXDSRequest(fakeServer.XDSRequestChan, wantReq, strconv.Itoa(ver-1), nonce); err != nil {
return fmt.Errorf("failed to receive %s request: %v", xdsname, err) return fmt.Errorf("failed to receive %s request: %v", xdsname, err)
} }
t.Logf("Bad %s response nacked", xdsname) t.Logf("Bad %s response nacked", xdsname)
@ -262,7 +266,7 @@ func (s) TestV2ClientAckFirstIsNack(t *testing.T) {
nonce := sendXDSRespWithVersion(fakeServer.XDSResponseChan, &xdspb.DiscoveryResponse{ nonce := sendXDSRespWithVersion(fakeServer.XDSResponseChan, &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{{}}, Resources: []*anypb.Any{{}},
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
}, versionLDS) }, versionLDS)
t.Logf("Bad response pushed to fakeServer...") t.Logf("Bad response pushed to fakeServer...")
@ -303,7 +307,7 @@ func (s) TestV2ClientAckNackAfterNewWatch(t *testing.T) {
// This is an invalid response after the new watch. // This is an invalid response after the new watch.
nonce = sendXDSRespWithVersion(fakeServer.XDSResponseChan, &xdspb.DiscoveryResponse{ nonce = sendXDSRespWithVersion(fakeServer.XDSResponseChan, &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{{}}, Resources: []*anypb.Any{{}},
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
}, versionLDS) }, versionLDS)
t.Logf("Bad response pushed to fakeServer...") t.Logf("Bad response pushed to fakeServer...")
@ -332,7 +336,7 @@ func (s) TestV2ClientAckNewWatchAfterCancel(t *testing.T) {
defer v2cCleanup() defer v2cCleanup()
// Start a CDS watch. // Start a CDS watch.
v2c.addWatch(cdsURL, goodClusterName1) v2c.AddWatch(version.V2ClusterURL, goodClusterName1)
if err := compareXDSRequest(fakeServer.XDSRequestChan, goodCDSRequest, "", ""); err != nil { if err := compareXDSRequest(fakeServer.XDSRequestChan, goodCDSRequest, "", ""); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -346,14 +350,14 @@ func (s) TestV2ClientAckNewWatchAfterCancel(t *testing.T) {
} }
// Cancel the CDS watch, and start a new one. The new watch should have the // Cancel the CDS watch, and start a new one. The new watch should have the
// version from the response above. // version from the response above.
v2c.removeWatch(cdsURL, goodClusterName1) v2c.RemoveWatch(version.V2ClusterURL, goodClusterName1)
// Wait for a request with no resource names, because the only watch was // Wait for a request with no resource names, because the only watch was
// removed. // removed.
emptyReq := &xdspb.DiscoveryRequest{Node: goodNodeProto, TypeUrl: cdsURL} emptyReq := &xdspb.DiscoveryRequest{Node: goodNodeProto, TypeUrl: version.V2ClusterURL}
if err := compareXDSRequest(fakeServer.XDSRequestChan, emptyReq, strconv.Itoa(versionCDS), nonce); err != nil { if err := compareXDSRequest(fakeServer.XDSRequestChan, emptyReq, strconv.Itoa(versionCDS), nonce); err != nil {
t.Fatalf("Failed to receive %s request: %v", "CDS", err) t.Fatalf("Failed to receive %s request: %v", "CDS", err)
} }
v2c.addWatch(cdsURL, goodClusterName1) v2c.AddWatch(version.V2ClusterURL, goodClusterName1)
// Wait for a request with correct resource names and version. // Wait for a request with correct resource names and version.
if err := compareXDSRequest(fakeServer.XDSRequestChan, goodCDSRequest, strconv.Itoa(versionCDS), nonce); err != nil { if err := compareXDSRequest(fakeServer.XDSRequestChan, goodCDSRequest, strconv.Itoa(versionCDS), nonce); err != nil {
t.Fatalf("Failed to receive %s request: %v", "CDS", err) t.Fatalf("Failed to receive %s request: %v", "CDS", err)
@ -387,7 +391,7 @@ func (s) TestV2ClientAckCancelResponseRace(t *testing.T) {
defer v2cCleanup() defer v2cCleanup()
// Start a CDS watch. // Start a CDS watch.
v2c.addWatch(cdsURL, goodClusterName1) v2c.AddWatch(version.V2ClusterURL, goodClusterName1)
if err := compareXDSRequest(fakeServer.XDSRequestChan, goodCDSRequest, "", ""); err != nil { if err := compareXDSRequest(fakeServer.XDSRequestChan, goodCDSRequest, "", ""); err != nil {
t.Fatalf("Failed to receive %s request: %v", "CDS", err) t.Fatalf("Failed to receive %s request: %v", "CDS", err)
} }
@ -400,10 +404,10 @@ func (s) TestV2ClientAckCancelResponseRace(t *testing.T) {
} }
// Cancel the watch before the next response is sent. This mimics the case // Cancel the watch before the next response is sent. This mimics the case
// watch is canceled while response is on wire. // watch is canceled while response is on wire.
v2c.removeWatch(cdsURL, goodClusterName1) v2c.RemoveWatch(version.V2ClusterURL, goodClusterName1)
// Wait for a request with no resource names, because the only watch was // Wait for a request with no resource names, because the only watch was
// removed. // removed.
emptyReq := &xdspb.DiscoveryRequest{Node: goodNodeProto, TypeUrl: cdsURL} emptyReq := &xdspb.DiscoveryRequest{Node: goodNodeProto, TypeUrl: version.V2ClusterURL}
if err := compareXDSRequest(fakeServer.XDSRequestChan, emptyReq, strconv.Itoa(versionCDS), nonce); err != nil { if err := compareXDSRequest(fakeServer.XDSRequestChan, emptyReq, strconv.Itoa(versionCDS), nonce); err != nil {
t.Fatalf("Failed to receive %s request: %v", "CDS", err) t.Fatalf("Failed to receive %s request: %v", "CDS", err)
} }
@ -428,7 +432,7 @@ func (s) TestV2ClientAckCancelResponseRace(t *testing.T) {
// Start a new watch. The new watch should have the nonce from the response // Start a new watch. The new watch should have the nonce from the response
// above, and version from the first good response. // above, and version from the first good response.
v2c.addWatch(cdsURL, goodClusterName1) v2c.AddWatch(version.V2ClusterURL, goodClusterName1)
if err := compareXDSRequest(fakeServer.XDSRequestChan, goodCDSRequest, strconv.Itoa(versionCDS-1), nonce); err != nil { if err := compareXDSRequest(fakeServer.XDSRequestChan, goodCDSRequest, strconv.Itoa(versionCDS-1), nonce); err != nil {
t.Fatalf("Failed to receive %s request: %v", "CDS", err) t.Fatalf("Failed to receive %s request: %v", "CDS", err)
} }

View File

@ -16,7 +16,7 @@
* *
*/ */
package client package v2
import ( import (
"testing" "testing"
@ -26,7 +26,8 @@ import (
corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
anypb "github.com/golang/protobuf/ptypes/any" anypb "github.com/golang/protobuf/ptypes/any"
"github.com/google/go-cmp/cmp" xdsclient "google.golang.org/grpc/xds/internal/client"
"google.golang.org/grpc/xds/internal/version"
) )
const ( const (
@ -34,115 +35,68 @@ const (
serviceName2 = "bar-service" serviceName2 = "bar-service"
) )
func (s) TestValidateCluster(t *testing.T) { var (
emptyUpdate := ClusterUpdate{ServiceName: "", EnableLRS: false} badlyMarshaledCDSResponse = &xdspb.DiscoveryResponse{
tests := []struct { Resources: []*anypb.Any{
name string {
cluster *xdspb.Cluster TypeUrl: version.V2ClusterURL,
wantUpdate ClusterUpdate Value: []byte{1, 2, 3, 4},
wantErr bool },
}{ },
{ TypeUrl: version.V2ClusterURL,
name: "non-eds-cluster-type", }
cluster: &xdspb.Cluster{ goodCluster1 = &xdspb.Cluster{
ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_STATIC}, Name: goodClusterName1,
EdsClusterConfig: &xdspb.Cluster_EdsClusterConfig{ ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS},
EdsConfig: &corepb.ConfigSource{ EdsClusterConfig: &xdspb.Cluster_EdsClusterConfig{
ConfigSourceSpecifier: &corepb.ConfigSource_Ads{ EdsConfig: &corepb.ConfigSource{
Ads: &corepb.AggregatedConfigSource{}, ConfigSourceSpecifier: &corepb.ConfigSource_Ads{
}, Ads: &corepb.AggregatedConfigSource{},
},
}, },
LbPolicy: xdspb.Cluster_LEAST_REQUEST,
}, },
wantUpdate: emptyUpdate, ServiceName: serviceName1,
wantErr: true,
}, },
{ LbPolicy: xdspb.Cluster_ROUND_ROBIN,
name: "no-eds-config", LrsServer: &corepb.ConfigSource{
cluster: &xdspb.Cluster{ ConfigSourceSpecifier: &corepb.ConfigSource_Self{
ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS}, Self: &corepb.SelfConfigSource{},
LbPolicy: xdspb.Cluster_ROUND_ROBIN,
}, },
wantUpdate: emptyUpdate,
wantErr: true,
},
{
name: "no-ads-config-source",
cluster: &xdspb.Cluster{
ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS},
EdsClusterConfig: &xdspb.Cluster_EdsClusterConfig{},
LbPolicy: xdspb.Cluster_ROUND_ROBIN,
},
wantUpdate: emptyUpdate,
wantErr: true,
},
{
name: "non-round-robin-lb-policy",
cluster: &xdspb.Cluster{
ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS},
EdsClusterConfig: &xdspb.Cluster_EdsClusterConfig{
EdsConfig: &corepb.ConfigSource{
ConfigSourceSpecifier: &corepb.ConfigSource_Ads{
Ads: &corepb.AggregatedConfigSource{},
},
},
},
LbPolicy: xdspb.Cluster_LEAST_REQUEST,
},
wantUpdate: emptyUpdate,
wantErr: true,
},
{
name: "happy-case-no-service-name-no-lrs",
cluster: &xdspb.Cluster{
ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS},
EdsClusterConfig: &xdspb.Cluster_EdsClusterConfig{
EdsConfig: &corepb.ConfigSource{
ConfigSourceSpecifier: &corepb.ConfigSource_Ads{
Ads: &corepb.AggregatedConfigSource{},
},
},
},
LbPolicy: xdspb.Cluster_ROUND_ROBIN,
},
wantUpdate: emptyUpdate,
},
{
name: "happy-case-no-lrs",
cluster: &xdspb.Cluster{
ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS},
EdsClusterConfig: &xdspb.Cluster_EdsClusterConfig{
EdsConfig: &corepb.ConfigSource{
ConfigSourceSpecifier: &corepb.ConfigSource_Ads{
Ads: &corepb.AggregatedConfigSource{},
},
},
ServiceName: serviceName1,
},
LbPolicy: xdspb.Cluster_ROUND_ROBIN,
},
wantUpdate: ClusterUpdate{ServiceName: serviceName1, EnableLRS: false},
},
{
name: "happiest-case",
cluster: goodCluster1,
wantUpdate: ClusterUpdate{ServiceName: serviceName1, EnableLRS: true},
}, },
} }
marshaledCluster1, _ = proto.Marshal(goodCluster1)
for _, test := range tests { goodCluster2 = &xdspb.Cluster{
t.Run(test.name, func(t *testing.T) { Name: goodClusterName2,
gotUpdate, gotErr := validateCluster(test.cluster) ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS},
if (gotErr != nil) != test.wantErr { EdsClusterConfig: &xdspb.Cluster_EdsClusterConfig{
t.Errorf("validateCluster(%+v) returned error: %v, wantErr: %v", test.cluster, gotErr, test.wantErr) EdsConfig: &corepb.ConfigSource{
} ConfigSourceSpecifier: &corepb.ConfigSource_Ads{
if !cmp.Equal(gotUpdate, test.wantUpdate) { Ads: &corepb.AggregatedConfigSource{},
t.Errorf("validateCluster(%+v) = %v, want: %v", test.cluster, gotUpdate, test.wantUpdate) },
} },
}) ServiceName: serviceName2,
},
LbPolicy: xdspb.Cluster_ROUND_ROBIN,
} }
} marshaledCluster2, _ = proto.Marshal(goodCluster2)
goodCDSResponse1 = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{
{
TypeUrl: version.V2ClusterURL,
Value: marshaledCluster1,
},
},
TypeUrl: version.V2ClusterURL,
}
goodCDSResponse2 = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{
{
TypeUrl: version.V2ClusterURL,
Value: marshaledCluster2,
},
},
TypeUrl: version.V2ClusterURL,
}
)
// TestCDSHandleResponse starts a fake xDS server, makes a ClientConn to it, // TestCDSHandleResponse starts a fake xDS server, makes a ClientConn to it,
// and creates a v2Client using it. Then, it registers a CDS watcher and tests // and creates a v2Client using it. Then, it registers a CDS watcher and tests
@ -152,7 +106,7 @@ func (s) TestCDSHandleResponse(t *testing.T) {
name string name string
cdsResponse *xdspb.DiscoveryResponse cdsResponse *xdspb.DiscoveryResponse
wantErr bool wantErr bool
wantUpdate *ClusterUpdate wantUpdate *xdsclient.ClusterUpdate
wantUpdateErr bool wantUpdateErr bool
}{ }{
// Badly marshaled CDS response. // Badly marshaled CDS response.
@ -192,14 +146,14 @@ func (s) TestCDSHandleResponse(t *testing.T) {
name: "one-good-cluster", name: "one-good-cluster",
cdsResponse: goodCDSResponse1, cdsResponse: goodCDSResponse1,
wantErr: false, wantErr: false,
wantUpdate: &ClusterUpdate{ServiceName: serviceName1, EnableLRS: true}, wantUpdate: &xdsclient.ClusterUpdate{ServiceName: serviceName1, EnableLRS: true},
wantUpdateErr: false, wantUpdateErr: false,
}, },
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
testWatchHandle(t, &watchHandleTestcase{ testWatchHandle(t, &watchHandleTestcase{
typeURL: cdsURL, typeURL: version.V2ClusterURL,
resourceName: goodClusterName1, resourceName: goodClusterName1,
responseToHandle: test.cdsResponse, responseToHandle: test.cdsResponse,
@ -217,10 +171,13 @@ func (s) TestCDSHandleResponseWithoutWatch(t *testing.T) {
_, cc, cleanup := startServerAndGetCC(t) _, cc, cleanup := startServerAndGetCC(t)
defer cleanup() defer cleanup()
v2c := newV2Client(&testUpdateReceiver{ v2c, err := newV2Client(&testUpdateReceiver{
f: func(string, map[string]interface{}) {}, f: func(string, map[string]interface{}) {},
}, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil) }, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil)
defer v2c.close() if err != nil {
t.Fatal(err)
}
defer v2c.Close()
if v2c.handleCDSResponse(badResourceTypeInLDSResponse) == nil { if v2c.handleCDSResponse(badResourceTypeInLDSResponse) == nil {
t.Fatal("v2c.handleCDSResponse() succeeded, should have failed") t.Fatal("v2c.handleCDSResponse() succeeded, should have failed")
@ -230,66 +187,3 @@ func (s) TestCDSHandleResponseWithoutWatch(t *testing.T) {
t.Fatal("v2c.handleCDSResponse() succeeded, should have failed") t.Fatal("v2c.handleCDSResponse() succeeded, should have failed")
} }
} }
var (
badlyMarshaledCDSResponse = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{
{
TypeUrl: cdsURL,
Value: []byte{1, 2, 3, 4},
},
},
TypeUrl: cdsURL,
}
goodCluster1 = &xdspb.Cluster{
Name: goodClusterName1,
ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS},
EdsClusterConfig: &xdspb.Cluster_EdsClusterConfig{
EdsConfig: &corepb.ConfigSource{
ConfigSourceSpecifier: &corepb.ConfigSource_Ads{
Ads: &corepb.AggregatedConfigSource{},
},
},
ServiceName: serviceName1,
},
LbPolicy: xdspb.Cluster_ROUND_ROBIN,
LrsServer: &corepb.ConfigSource{
ConfigSourceSpecifier: &corepb.ConfigSource_Self{
Self: &corepb.SelfConfigSource{},
},
},
}
marshaledCluster1, _ = proto.Marshal(goodCluster1)
goodCluster2 = &xdspb.Cluster{
Name: goodClusterName2,
ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS},
EdsClusterConfig: &xdspb.Cluster_EdsClusterConfig{
EdsConfig: &corepb.ConfigSource{
ConfigSourceSpecifier: &corepb.ConfigSource_Ads{
Ads: &corepb.AggregatedConfigSource{},
},
},
ServiceName: serviceName2,
},
LbPolicy: xdspb.Cluster_ROUND_ROBIN,
}
marshaledCluster2, _ = proto.Marshal(goodCluster2)
goodCDSResponse1 = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{
{
TypeUrl: cdsURL,
Value: marshaledCluster1,
},
},
TypeUrl: cdsURL,
}
goodCDSResponse2 = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{
{
TypeUrl: cdsURL,
Value: marshaledCluster2,
},
},
TypeUrl: cdsURL,
}
)

View File

@ -13,157 +13,75 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package client package v2
import ( import (
"testing" "testing"
"time" "time"
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2" v2xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
"github.com/golang/protobuf/ptypes" "github.com/golang/protobuf/ptypes"
anypb "github.com/golang/protobuf/ptypes/any" anypb "github.com/golang/protobuf/ptypes/any"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/xds/internal" "google.golang.org/grpc/xds/internal"
xdsclient "google.golang.org/grpc/xds/internal/client"
"google.golang.org/grpc/xds/internal/testutils"
"google.golang.org/grpc/xds/internal/version"
) )
func (s) TestEDSParseRespProto(t *testing.T) {
tests := []struct {
name string
m *xdspb.ClusterLoadAssignment
want EndpointsUpdate
wantErr bool
}{
{
name: "missing-priority",
m: func() *xdspb.ClusterLoadAssignment {
clab0 := NewClusterLoadAssignmentBuilder("test", nil)
clab0.AddLocality("locality-1", 1, 0, []string{"addr1:314"}, nil)
clab0.AddLocality("locality-2", 1, 2, []string{"addr2:159"}, nil)
return clab0.Build()
}(),
want: EndpointsUpdate{},
wantErr: true,
},
{
name: "missing-locality-ID",
m: func() *xdspb.ClusterLoadAssignment {
clab0 := NewClusterLoadAssignmentBuilder("test", nil)
clab0.AddLocality("", 1, 0, []string{"addr1:314"}, nil)
return clab0.Build()
}(),
want: EndpointsUpdate{},
wantErr: true,
},
{
name: "good",
m: func() *xdspb.ClusterLoadAssignment {
clab0 := NewClusterLoadAssignmentBuilder("test", nil)
clab0.AddLocality("locality-1", 1, 1, []string{"addr1:314"}, &AddLocalityOptions{
Health: []corepb.HealthStatus{corepb.HealthStatus_UNHEALTHY},
Weight: []uint32{271},
})
clab0.AddLocality("locality-2", 1, 0, []string{"addr2:159"}, &AddLocalityOptions{
Health: []corepb.HealthStatus{corepb.HealthStatus_DRAINING},
Weight: []uint32{828},
})
return clab0.Build()
}(),
want: EndpointsUpdate{
Drops: nil,
Localities: []Locality{
{
Endpoints: []Endpoint{{
Address: "addr1:314",
HealthStatus: EndpointHealthStatusUnhealthy,
Weight: 271,
}},
ID: internal.LocalityID{SubZone: "locality-1"},
Priority: 1,
Weight: 1,
},
{
Endpoints: []Endpoint{{
Address: "addr2:159",
HealthStatus: EndpointHealthStatusDraining,
Weight: 828,
}},
ID: internal.LocalityID{SubZone: "locality-2"},
Priority: 0,
Weight: 1,
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseEDSRespProto(tt.m)
if (err != nil) != tt.wantErr {
t.Errorf("ParseEDSRespProto() error = %v, wantErr %v", err, tt.wantErr)
return
}
if d := cmp.Diff(got, tt.want); d != "" {
t.Errorf("ParseEDSRespProto() got = %v, want %v, diff: %v", got, tt.want, d)
}
})
}
}
var ( var (
badlyMarshaledEDSResponse = &xdspb.DiscoveryResponse{ badlyMarshaledEDSResponse = &v2xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
{ {
TypeUrl: edsURL, TypeUrl: version.V2EndpointsURL,
Value: []byte{1, 2, 3, 4}, Value: []byte{1, 2, 3, 4},
}, },
}, },
TypeUrl: edsURL, TypeUrl: version.V2EndpointsURL,
} }
badResourceTypeInEDSResponse = &xdspb.DiscoveryResponse{ badResourceTypeInEDSResponse = &v2xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
{ {
TypeUrl: httpConnManagerURL, TypeUrl: httpConnManagerURL,
Value: marshaledConnMgr1, Value: marshaledConnMgr1,
}, },
}, },
TypeUrl: edsURL, TypeUrl: version.V2EndpointsURL,
} }
goodEDSResponse1 = &xdspb.DiscoveryResponse{ goodEDSResponse1 = &v2xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
func() *anypb.Any { func() *anypb.Any {
clab0 := NewClusterLoadAssignmentBuilder(goodEDSName, nil) clab0 := testutils.NewClusterLoadAssignmentBuilder(goodEDSName, nil)
clab0.AddLocality("locality-1", 1, 1, []string{"addr1:314"}, nil) clab0.AddLocality("locality-1", 1, 1, []string{"addr1:314"}, nil)
clab0.AddLocality("locality-2", 1, 0, []string{"addr2:159"}, nil) clab0.AddLocality("locality-2", 1, 0, []string{"addr2:159"}, nil)
a, _ := ptypes.MarshalAny(clab0.Build()) a, _ := ptypes.MarshalAny(clab0.Build())
return a return a
}(), }(),
}, },
TypeUrl: edsURL, TypeUrl: version.V2EndpointsURL,
} }
goodEDSResponse2 = &xdspb.DiscoveryResponse{ goodEDSResponse2 = &v2xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
func() *anypb.Any { func() *anypb.Any {
clab0 := NewClusterLoadAssignmentBuilder("not-goodEDSName", nil) clab0 := testutils.NewClusterLoadAssignmentBuilder("not-goodEDSName", nil)
clab0.AddLocality("locality-1", 1, 1, []string{"addr1:314"}, nil) clab0.AddLocality("locality-1", 1, 1, []string{"addr1:314"}, nil)
clab0.AddLocality("locality-2", 1, 0, []string{"addr2:159"}, nil) clab0.AddLocality("locality-2", 1, 0, []string{"addr2:159"}, nil)
a, _ := ptypes.MarshalAny(clab0.Build()) a, _ := ptypes.MarshalAny(clab0.Build())
return a return a
}(), }(),
}, },
TypeUrl: edsURL, TypeUrl: version.V2EndpointsURL,
} }
) )
func (s) TestEDSHandleResponse(t *testing.T) { func (s) TestEDSHandleResponse(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
edsResponse *xdspb.DiscoveryResponse edsResponse *v2xdspb.DiscoveryResponse
wantErr bool wantErr bool
wantUpdate *EndpointsUpdate wantUpdate *xdsclient.EndpointsUpdate
wantUpdateErr bool wantUpdateErr bool
}{ }{
// Any in resource is badly marshaled. // Any in resource is badly marshaled.
@ -195,16 +113,16 @@ func (s) TestEDSHandleResponse(t *testing.T) {
name: "one-good-assignment", name: "one-good-assignment",
edsResponse: goodEDSResponse1, edsResponse: goodEDSResponse1,
wantErr: false, wantErr: false,
wantUpdate: &EndpointsUpdate{ wantUpdate: &xdsclient.EndpointsUpdate{
Localities: []Locality{ Localities: []xdsclient.Locality{
{ {
Endpoints: []Endpoint{{Address: "addr1:314"}}, Endpoints: []xdsclient.Endpoint{{Address: "addr1:314"}},
ID: internal.LocalityID{SubZone: "locality-1"}, ID: internal.LocalityID{SubZone: "locality-1"},
Priority: 1, Priority: 1,
Weight: 1, Weight: 1,
}, },
{ {
Endpoints: []Endpoint{{Address: "addr2:159"}}, Endpoints: []xdsclient.Endpoint{{Address: "addr2:159"}},
ID: internal.LocalityID{SubZone: "locality-2"}, ID: internal.LocalityID{SubZone: "locality-2"},
Priority: 0, Priority: 0,
Weight: 1, Weight: 1,
@ -217,7 +135,7 @@ func (s) TestEDSHandleResponse(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
testWatchHandle(t, &watchHandleTestcase{ testWatchHandle(t, &watchHandleTestcase{
typeURL: edsURL, typeURL: version.V2EndpointsURL,
resourceName: goodEDSName, resourceName: goodEDSName,
responseToHandle: test.edsResponse, responseToHandle: test.edsResponse,
wantHandleErr: test.wantErr, wantHandleErr: test.wantErr,
@ -234,10 +152,13 @@ func (s) TestEDSHandleResponseWithoutWatch(t *testing.T) {
_, cc, cleanup := startServerAndGetCC(t) _, cc, cleanup := startServerAndGetCC(t)
defer cleanup() defer cleanup()
v2c := newV2Client(&testUpdateReceiver{ v2c, err := newV2Client(&testUpdateReceiver{
f: func(string, map[string]interface{}) {}, f: func(string, map[string]interface{}) {},
}, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil) }, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil)
defer v2c.close() if err != nil {
t.Fatal(err)
}
defer v2c.Close()
if v2c.handleEDSResponse(badResourceTypeInEDSResponse) == nil { if v2c.handleEDSResponse(badResourceTypeInEDSResponse) == nil {
t.Fatal("v2c.handleEDSResponse() succeeded, should have failed") t.Fatal("v2c.handleEDSResponse() succeeded, should have failed")

View File

@ -16,114 +16,26 @@
* *
*/ */
package client package v2
import ( import (
"testing" "testing"
"time" "time"
"github.com/golang/protobuf/proto" v2xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
xdsclient "google.golang.org/grpc/xds/internal/client"
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2" "google.golang.org/grpc/xds/internal/version"
basepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
httppb "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2"
listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v2"
anypb "github.com/golang/protobuf/ptypes/any"
) )
func (s) TestLDSGetRouteConfig(t *testing.T) {
tests := []struct {
name string
lis *xdspb.Listener
wantRoute string
wantErr bool
}{
{
name: "no-apiListener-field",
lis: &xdspb.Listener{},
wantRoute: "",
wantErr: true,
},
{
name: "badly-marshaled-apiListener",
lis: badAPIListener1,
wantRoute: "",
wantErr: true,
},
{
name: "wrong-type-in-apiListener",
lis: badResourceListener,
wantRoute: "",
wantErr: true,
},
{
name: "empty-httpConnMgr-in-apiListener",
lis: listenerWithEmptyHTTPConnMgr,
wantRoute: "",
wantErr: true,
},
{
name: "scopedRoutes-routeConfig-in-apiListener",
lis: listenerWithScopedRoutesRouteConfig,
wantRoute: "",
wantErr: true,
},
{
name: "rds.ConfigSource-in-apiListener-is-not-ADS",
lis: &xdspb.Listener{
Name: goodLDSTarget1,
ApiListener: &listenerpb.ApiListener{
ApiListener: &anypb.Any{
TypeUrl: httpConnManagerURL,
Value: func() []byte {
cm := &httppb.HttpConnectionManager{
RouteSpecifier: &httppb.HttpConnectionManager_Rds{
Rds: &httppb.Rds{
ConfigSource: &basepb.ConfigSource{
ConfigSourceSpecifier: &basepb.ConfigSource_Path{
Path: "/some/path",
},
},
RouteConfigName: goodRouteName1}}}
mcm, _ := proto.Marshal(cm)
return mcm
}()}}},
wantRoute: "",
wantErr: true,
},
{
name: "goodListener1",
lis: goodListener1,
wantRoute: goodRouteName1,
wantErr: false,
},
}
_, cc, cleanup := startServerAndGetCC(t)
defer cleanup()
v2c := newV2Client(nil, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil)
defer v2c.close()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
gotRoute, err := v2c.getRouteConfigNameFromListener(test.lis)
if gotRoute != test.wantRoute {
t.Errorf("getRouteConfigNameFromListener(%+v) = %v, want %v", test.lis, gotRoute, test.wantRoute)
}
if (err != nil) != test.wantErr {
t.Errorf("getRouteConfigNameFromListener(%+v) = %v, want %v", test.lis, err, test.wantErr)
}
})
}
}
// TestLDSHandleResponse starts a fake xDS server, makes a ClientConn to it, // TestLDSHandleResponse starts a fake xDS server, makes a ClientConn to it,
// and creates a v2Client using it. Then, it registers a watchLDS and tests // and creates a client using it. Then, it registers a watchLDS and tests
// different LDS responses. // different LDS responses.
func (s) TestLDSHandleResponse(t *testing.T) { func (s) TestLDSHandleResponse(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
ldsResponse *xdspb.DiscoveryResponse ldsResponse *v2xdspb.DiscoveryResponse
wantErr bool wantErr bool
wantUpdate *ldsUpdate wantUpdate *xdsclient.ListenerUpdate
wantUpdateErr bool wantUpdateErr bool
}{ }{
// Badly marshaled LDS response. // Badly marshaled LDS response.
@ -157,7 +69,7 @@ func (s) TestLDSHandleResponse(t *testing.T) {
name: "one-good-listener", name: "one-good-listener",
ldsResponse: goodLDSResponse1, ldsResponse: goodLDSResponse1,
wantErr: false, wantErr: false,
wantUpdate: &ldsUpdate{routeName: goodRouteName1}, wantUpdate: &xdsclient.ListenerUpdate{RouteConfigName: goodRouteName1},
wantUpdateErr: false, wantUpdateErr: false,
}, },
// Response contains multiple good listeners, including the one we are // Response contains multiple good listeners, including the one we are
@ -166,7 +78,7 @@ func (s) TestLDSHandleResponse(t *testing.T) {
name: "multiple-good-listener", name: "multiple-good-listener",
ldsResponse: ldsResponseWithMultipleResources, ldsResponse: ldsResponseWithMultipleResources,
wantErr: false, wantErr: false,
wantUpdate: &ldsUpdate{routeName: goodRouteName1}, wantUpdate: &xdsclient.ListenerUpdate{RouteConfigName: goodRouteName1},
wantUpdateErr: false, wantUpdateErr: false,
}, },
// Response contains two good listeners (one interesting and one // Response contains two good listeners (one interesting and one
@ -201,7 +113,7 @@ func (s) TestLDSHandleResponse(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
testWatchHandle(t, &watchHandleTestcase{ testWatchHandle(t, &watchHandleTestcase{
typeURL: ldsURL, typeURL: version.V2ListenerURL,
resourceName: goodLDSTarget1, resourceName: goodLDSTarget1,
responseToHandle: test.ldsResponse, responseToHandle: test.ldsResponse,
wantHandleErr: test.wantErr, wantHandleErr: test.wantErr,
@ -212,16 +124,19 @@ func (s) TestLDSHandleResponse(t *testing.T) {
} }
} }
// TestLDSHandleResponseWithoutWatch tests the case where the v2Client receives // TestLDSHandleResponseWithoutWatch tests the case where the client receives
// an LDS response without a registered watcher. // an LDS response without a registered watcher.
func (s) TestLDSHandleResponseWithoutWatch(t *testing.T) { func (s) TestLDSHandleResponseWithoutWatch(t *testing.T) {
_, cc, cleanup := startServerAndGetCC(t) _, cc, cleanup := startServerAndGetCC(t)
defer cleanup() defer cleanup()
v2c := newV2Client(&testUpdateReceiver{ v2c, err := newV2Client(&testUpdateReceiver{
f: func(string, map[string]interface{}) {}, f: func(string, map[string]interface{}) {},
}, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil) }, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil)
defer v2c.close() if err != nil {
t.Fatal(err)
}
defer v2c.Close()
if v2c.handleLDSResponse(badResourceTypeInLDSResponse) == nil { if v2c.handleLDSResponse(badResourceTypeInLDSResponse) == nil {
t.Fatal("v2c.handleLDSResponse() succeeded, should have failed") t.Fatal("v2c.handleLDSResponse() succeeded, should have failed")

View File

@ -0,0 +1,167 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package v2
import (
"testing"
"time"
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
xdsclient "google.golang.org/grpc/xds/internal/client"
"google.golang.org/grpc/xds/internal/testutils/fakeserver"
"google.golang.org/grpc/xds/internal/version"
)
// doLDS makes a LDS watch, and waits for the response and ack to finish.
//
// This is called by RDS tests to start LDS first, because LDS is a
// pre-requirement for RDS, and RDS handle would fail without an existing LDS
// watch.
func doLDS(t *testing.T, v2c xdsclient.APIClient, fakeServer *fakeserver.Server) {
v2c.AddWatch(version.V2ListenerURL, goodLDSTarget1)
if _, err := fakeServer.XDSRequestChan.Receive(); err != nil {
t.Fatalf("Timeout waiting for LDS request: %v", err)
}
}
// TestRDSHandleResponseWithRouting starts a fake xDS server, makes a ClientConn
// to it, and creates a v2Client using it. Then, it registers an LDS and RDS
// watcher and tests different RDS responses.
func (s) TestRDSHandleResponseWithRouting(t *testing.T) {
tests := []struct {
name string
rdsResponse *xdspb.DiscoveryResponse
wantErr bool
wantUpdate *xdsclient.RouteConfigUpdate
wantUpdateErr bool
}{
// Badly marshaled RDS response.
{
name: "badly-marshaled-response",
rdsResponse: badlyMarshaledRDSResponse,
wantErr: true,
wantUpdate: nil,
wantUpdateErr: false,
},
// Response does not contain RouteConfiguration proto.
{
name: "no-route-config-in-response",
rdsResponse: badResourceTypeInRDSResponse,
wantErr: true,
wantUpdate: nil,
wantUpdateErr: false,
},
// No VirtualHosts in the response. Just one test case here for a bad
// RouteConfiguration, since the others are covered in
// TestGetClusterFromRouteConfiguration.
{
name: "no-virtual-hosts-in-response",
rdsResponse: noVirtualHostsInRDSResponse,
wantErr: true,
wantUpdate: nil,
wantUpdateErr: false,
},
// Response contains one good RouteConfiguration, uninteresting though.
{
name: "one-uninteresting-route-config",
rdsResponse: goodRDSResponse2,
wantErr: false,
wantUpdate: nil,
wantUpdateErr: false,
},
// Response contains one good interesting RouteConfiguration.
{
name: "one-good-route-config",
rdsResponse: goodRDSResponse1,
wantErr: false,
wantUpdate: &xdsclient.RouteConfigUpdate{Routes: []*xdsclient.Route{{Prefix: newStringP(""), Action: map[string]uint32{goodClusterName1: 1}}}},
wantUpdateErr: false,
},
{
name: "one-good-route-config with routes",
rdsResponse: goodRDSResponse1,
wantErr: false,
wantUpdate: &xdsclient.RouteConfigUpdate{
// Instead of just weighted targets when routing is disabled,
// this result contains a route with perfix "", and action as
// weighted targets.
Routes: []*xdsclient.Route{{
Prefix: newStringP(""),
Action: map[string]uint32{goodClusterName1: 1},
}},
},
wantUpdateErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testWatchHandle(t, &watchHandleTestcase{
typeURL: version.V2RouteConfigURL,
resourceName: goodRouteName1,
responseToHandle: test.rdsResponse,
wantHandleErr: test.wantErr,
wantUpdate: test.wantUpdate,
wantUpdateErr: test.wantUpdateErr,
})
})
}
}
// TestRDSHandleResponseWithoutLDSWatch tests the case where the v2Client
// receives an RDS response without a registered LDS watcher.
func (s) TestRDSHandleResponseWithoutLDSWatch(t *testing.T) {
_, cc, cleanup := startServerAndGetCC(t)
defer cleanup()
v2c, err := newV2Client(&testUpdateReceiver{
f: func(string, map[string]interface{}) {},
}, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil)
if err != nil {
t.Fatal(err)
}
defer v2c.Close()
if v2c.handleRDSResponse(goodRDSResponse1) == nil {
t.Fatal("v2c.handleRDSResponse() succeeded, should have failed")
}
}
// TestRDSHandleResponseWithoutRDSWatch tests the case where the v2Client
// receives an RDS response without a registered RDS watcher.
func (s) TestRDSHandleResponseWithoutRDSWatch(t *testing.T) {
fakeServer, cc, cleanup := startServerAndGetCC(t)
defer cleanup()
v2c, err := newV2Client(&testUpdateReceiver{
f: func(string, map[string]interface{}) {},
}, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil)
if err != nil {
t.Fatal(err)
}
defer v2c.Close()
doLDS(t, v2c, fakeServer)
if v2c.handleRDSResponse(badResourceTypeInRDSResponse) == nil {
t.Fatal("v2c.handleRDSResponse() succeeded, should have failed")
}
if v2c.handleRDSResponse(goodRDSResponse1) != nil {
t.Fatal("v2c.handleRDSResponse() succeeded, should have failed")
}
}

View File

@ -16,19 +16,25 @@
* *
*/ */
package client package v2
import ( import (
"errors" "errors"
"reflect"
"testing" "testing"
"time" "time"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/internal/grpclog"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/resolver" "google.golang.org/grpc/resolver"
"google.golang.org/grpc/resolver/manual" "google.golang.org/grpc/resolver/manual"
xdsclient "google.golang.org/grpc/xds/internal/client"
"google.golang.org/grpc/xds/internal/testutils" "google.golang.org/grpc/xds/internal/testutils"
"google.golang.org/grpc/xds/internal/testutils/fakeserver" "google.golang.org/grpc/xds/internal/testutils/fakeserver"
"google.golang.org/grpc/xds/internal/version"
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2" xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
basepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" basepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
@ -39,8 +45,15 @@ import (
structpb "github.com/golang/protobuf/ptypes/struct" structpb "github.com/golang/protobuf/ptypes/struct"
) )
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
const ( const (
defaultTestTimeout = 1 * time.Second
goodLDSTarget1 = "lds.target.good:1111" goodLDSTarget1 = "lds.target.good:1111"
goodLDSTarget2 = "lds.target.good:2222" goodLDSTarget2 = "lds.target.good:2222"
goodRouteName1 = "GoodRouteConfig1" goodRouteName1 = "GoodRouteConfig1"
@ -67,22 +80,22 @@ var (
} }
goodLDSRequest = &xdspb.DiscoveryRequest{ goodLDSRequest = &xdspb.DiscoveryRequest{
Node: goodNodeProto, Node: goodNodeProto,
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
ResourceNames: []string{goodLDSTarget1}, ResourceNames: []string{goodLDSTarget1},
} }
goodRDSRequest = &xdspb.DiscoveryRequest{ goodRDSRequest = &xdspb.DiscoveryRequest{
Node: goodNodeProto, Node: goodNodeProto,
TypeUrl: rdsURL, TypeUrl: version.V2RouteConfigURL,
ResourceNames: []string{goodRouteName1}, ResourceNames: []string{goodRouteName1},
} }
goodCDSRequest = &xdspb.DiscoveryRequest{ goodCDSRequest = &xdspb.DiscoveryRequest{
Node: goodNodeProto, Node: goodNodeProto,
TypeUrl: cdsURL, TypeUrl: version.V2ClusterURL,
ResourceNames: []string{goodClusterName1}, ResourceNames: []string{goodClusterName1},
} }
goodEDSRequest = &xdspb.DiscoveryRequest{ goodEDSRequest = &xdspb.DiscoveryRequest{
Node: goodNodeProto, Node: goodNodeProto,
TypeUrl: edsURL, TypeUrl: version.V2EndpointsURL,
ResourceNames: []string{goodEDSName}, ResourceNames: []string{goodEDSName},
} }
goodHTTPConnManager1 = &httppb.HttpConnectionManager{ goodHTTPConnManager1 = &httppb.HttpConnectionManager{
@ -96,17 +109,7 @@ var (
}, },
} }
marshaledConnMgr1, _ = proto.Marshal(goodHTTPConnManager1) marshaledConnMgr1, _ = proto.Marshal(goodHTTPConnManager1)
emptyHTTPConnManager = &httppb.HttpConnectionManager{ goodListener1 = &xdspb.Listener{
RouteSpecifier: &httppb.HttpConnectionManager_Rds{
Rds: &httppb.Rds{},
},
}
emptyMarshaledConnMgr, _ = proto.Marshal(emptyHTTPConnManager)
connMgrWithScopedRoutes = &httppb.HttpConnectionManager{
RouteSpecifier: &httppb.HttpConnectionManager_ScopedRoutes{},
}
marshaledConnMgrWithScopedRoutes, _ = proto.Marshal(connMgrWithScopedRoutes)
goodListener1 = &xdspb.Listener{
Name: goodLDSTarget1, Name: goodLDSTarget1,
ApiListener: &listenerpb.ApiListener{ ApiListener: &listenerpb.ApiListener{
ApiListener: &anypb.Any{ ApiListener: &anypb.Any{
@ -128,16 +131,7 @@ var (
marshaledListener2, _ = proto.Marshal(goodListener2) marshaledListener2, _ = proto.Marshal(goodListener2)
noAPIListener = &xdspb.Listener{Name: goodLDSTarget1} noAPIListener = &xdspb.Listener{Name: goodLDSTarget1}
marshaledNoAPIListener, _ = proto.Marshal(noAPIListener) marshaledNoAPIListener, _ = proto.Marshal(noAPIListener)
badAPIListener1 = &xdspb.Listener{ badAPIListener2 = &xdspb.Listener{
Name: goodLDSTarget1,
ApiListener: &listenerpb.ApiListener{
ApiListener: &anypb.Any{
TypeUrl: httpConnManagerURL,
Value: []byte{1, 2, 3, 4},
},
},
}
badAPIListener2 = &xdspb.Listener{
Name: goodLDSTarget2, Name: goodLDSTarget2,
ApiListener: &listenerpb.ApiListener{ ApiListener: &listenerpb.ApiListener{
ApiListener: &anypb.Any{ ApiListener: &anypb.Any{
@ -147,60 +141,33 @@ var (
}, },
} }
badlyMarshaledAPIListener2, _ = proto.Marshal(badAPIListener2) badlyMarshaledAPIListener2, _ = proto.Marshal(badAPIListener2)
badResourceListener = &xdspb.Listener{ goodLDSResponse1 = &xdspb.DiscoveryResponse{
Name: goodLDSTarget1,
ApiListener: &listenerpb.ApiListener{
ApiListener: &anypb.Any{
TypeUrl: ldsURL,
Value: marshaledListener1,
},
},
}
listenerWithEmptyHTTPConnMgr = &xdspb.Listener{
Name: goodLDSTarget1,
ApiListener: &listenerpb.ApiListener{
ApiListener: &anypb.Any{
TypeUrl: httpConnManagerURL,
Value: emptyMarshaledConnMgr,
},
},
}
listenerWithScopedRoutesRouteConfig = &xdspb.Listener{
Name: goodLDSTarget1,
ApiListener: &listenerpb.ApiListener{
ApiListener: &anypb.Any{
TypeUrl: httpConnManagerURL,
Value: marshaledConnMgrWithScopedRoutes,
},
},
}
goodLDSResponse1 = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
{ {
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
Value: marshaledListener1, Value: marshaledListener1,
}, },
}, },
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
} }
goodLDSResponse2 = &xdspb.DiscoveryResponse{ goodLDSResponse2 = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
{ {
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
Value: marshaledListener2, Value: marshaledListener2,
}, },
}, },
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
} }
emptyLDSResponse = &xdspb.DiscoveryResponse{TypeUrl: ldsURL} emptyLDSResponse = &xdspb.DiscoveryResponse{TypeUrl: version.V2ListenerURL}
badlyMarshaledLDSResponse = &xdspb.DiscoveryResponse{ badlyMarshaledLDSResponse = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
{ {
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
Value: []byte{1, 2, 3, 4}, Value: []byte{1, 2, 3, 4},
}, },
}, },
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
} }
badResourceTypeInLDSResponse = &xdspb.DiscoveryResponse{ badResourceTypeInLDSResponse = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
@ -209,55 +176,55 @@ var (
Value: marshaledConnMgr1, Value: marshaledConnMgr1,
}, },
}, },
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
} }
ldsResponseWithMultipleResources = &xdspb.DiscoveryResponse{ ldsResponseWithMultipleResources = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
{ {
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
Value: marshaledListener2, Value: marshaledListener2,
}, },
{ {
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
Value: marshaledListener1, Value: marshaledListener1,
}, },
}, },
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
} }
noAPIListenerLDSResponse = &xdspb.DiscoveryResponse{ noAPIListenerLDSResponse = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
{ {
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
Value: marshaledNoAPIListener, Value: marshaledNoAPIListener,
}, },
}, },
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
} }
goodBadUglyLDSResponse = &xdspb.DiscoveryResponse{ goodBadUglyLDSResponse = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
{ {
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
Value: marshaledListener2, Value: marshaledListener2,
}, },
{ {
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
Value: marshaledListener1, Value: marshaledListener1,
}, },
{ {
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
Value: badlyMarshaledAPIListener2, Value: badlyMarshaledAPIListener2,
}, },
}, },
TypeUrl: ldsURL, TypeUrl: version.V2ListenerURL,
} }
badlyMarshaledRDSResponse = &xdspb.DiscoveryResponse{ badlyMarshaledRDSResponse = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
{ {
TypeUrl: rdsURL, TypeUrl: version.V2RouteConfigURL,
Value: []byte{1, 2, 3, 4}, Value: []byte{1, 2, 3, 4},
}, },
}, },
TypeUrl: rdsURL, TypeUrl: version.V2RouteConfigURL,
} }
badResourceTypeInRDSResponse = &xdspb.DiscoveryResponse{ badResourceTypeInRDSResponse = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
@ -266,21 +233,18 @@ var (
Value: marshaledConnMgr1, Value: marshaledConnMgr1,
}, },
}, },
TypeUrl: rdsURL, TypeUrl: version.V2RouteConfigURL,
} }
emptyRouteConfig = &xdspb.RouteConfiguration{} emptyRouteConfig = &xdspb.RouteConfiguration{}
marshaledEmptyRouteConfig, _ = proto.Marshal(emptyRouteConfig) marshaledEmptyRouteConfig, _ = proto.Marshal(emptyRouteConfig)
noDomainsInRouteConfig = &xdspb.RouteConfiguration{ noVirtualHostsInRDSResponse = &xdspb.DiscoveryResponse{
VirtualHosts: []*routepb.VirtualHost{{}},
}
noVirtualHostsInRDSResponse = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
{ {
TypeUrl: rdsURL, TypeUrl: version.V2RouteConfigURL,
Value: marshaledEmptyRouteConfig, Value: marshaledEmptyRouteConfig,
}, },
}, },
TypeUrl: rdsURL, TypeUrl: version.V2RouteConfigURL,
} }
goodRouteConfig1 = &xdspb.RouteConfiguration{ goodRouteConfig1 = &xdspb.RouteConfiguration{
Name: goodRouteName1, Name: goodRouteName1,
@ -349,23 +313,201 @@ var (
goodRDSResponse1 = &xdspb.DiscoveryResponse{ goodRDSResponse1 = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
{ {
TypeUrl: rdsURL, TypeUrl: version.V2RouteConfigURL,
Value: marshaledGoodRouteConfig1, Value: marshaledGoodRouteConfig1,
}, },
}, },
TypeUrl: rdsURL, TypeUrl: version.V2RouteConfigURL,
} }
goodRDSResponse2 = &xdspb.DiscoveryResponse{ goodRDSResponse2 = &xdspb.DiscoveryResponse{
Resources: []*anypb.Any{ Resources: []*anypb.Any{
{ {
TypeUrl: rdsURL, TypeUrl: version.V2RouteConfigURL,
Value: marshaledGoodRouteConfig2, Value: marshaledGoodRouteConfig2,
}, },
}, },
TypeUrl: rdsURL, TypeUrl: version.V2RouteConfigURL,
} }
) )
type watchHandleTestcase struct {
typeURL string
resourceName string
responseToHandle *xdspb.DiscoveryResponse
wantHandleErr bool
wantUpdate interface{}
wantUpdateErr bool
}
type testUpdateReceiver struct {
f func(typeURL string, d map[string]interface{})
}
func (t *testUpdateReceiver) NewListeners(d map[string]xdsclient.ListenerUpdate) {
dd := make(map[string]interface{})
for k, v := range d {
dd[k] = v
}
t.newUpdate(version.V2ListenerURL, dd)
}
func (t *testUpdateReceiver) NewRouteConfigs(d map[string]xdsclient.RouteConfigUpdate) {
dd := make(map[string]interface{})
for k, v := range d {
dd[k] = v
}
t.newUpdate(version.V2RouteConfigURL, dd)
}
func (t *testUpdateReceiver) NewClusters(d map[string]xdsclient.ClusterUpdate) {
dd := make(map[string]interface{})
for k, v := range d {
dd[k] = v
}
t.newUpdate(version.V2ClusterURL, dd)
}
func (t *testUpdateReceiver) NewEndpoints(d map[string]xdsclient.EndpointsUpdate) {
dd := make(map[string]interface{})
for k, v := range d {
dd[k] = v
}
t.newUpdate(version.V2EndpointsURL, dd)
}
func (t *testUpdateReceiver) newUpdate(typeURL string, d map[string]interface{}) {
t.f(typeURL, d)
}
// testWatchHandle is called to test response handling for each xDS.
//
// It starts the xDS watch as configured in test, waits for the fake xds server
// to receive the request (so watch callback is installed), and calls
// handleXDSResp with responseToHandle (if it's set). It then compares the
// update received by watch callback with the expected results.
func testWatchHandle(t *testing.T, test *watchHandleTestcase) {
fakeServer, cc, cleanup := startServerAndGetCC(t)
defer cleanup()
type updateErr struct {
u interface{}
err error
}
gotUpdateCh := testutils.NewChannel()
v2c, err := newV2Client(&testUpdateReceiver{
f: func(typeURL string, d map[string]interface{}) {
if typeURL == test.typeURL {
if u, ok := d[test.resourceName]; ok {
gotUpdateCh.Send(updateErr{u, nil})
}
}
},
}, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil)
if err != nil {
t.Fatal(err)
}
defer v2c.Close()
// RDS needs an existin LDS watch for the hostname.
if test.typeURL == version.V2RouteConfigURL {
doLDS(t, v2c, fakeServer)
}
// Register the watcher, this will also trigger the v2Client to send the xDS
// request.
v2c.AddWatch(test.typeURL, test.resourceName)
// Wait till the request makes it to the fakeServer. This ensures that
// the watch request has been processed by the v2Client.
if _, err := fakeServer.XDSRequestChan.Receive(); err != nil {
t.Fatalf("Timeout waiting for an xDS request: %v", err)
}
// Directly push the response through a call to handleXDSResp. This bypasses
// the fakeServer, so it's only testing the handle logic. Client response
// processing is covered elsewhere.
//
// Also note that this won't trigger ACK, so there's no need to clear the
// request channel afterwards.
var handleXDSResp func(response *xdspb.DiscoveryResponse) error
switch test.typeURL {
case version.V2ListenerURL:
handleXDSResp = v2c.handleLDSResponse
case version.V2RouteConfigURL:
handleXDSResp = v2c.handleRDSResponse
case version.V2ClusterURL:
handleXDSResp = v2c.handleCDSResponse
case version.V2EndpointsURL:
handleXDSResp = v2c.handleEDSResponse
}
if err := handleXDSResp(test.responseToHandle); (err != nil) != test.wantHandleErr {
t.Fatalf("v2c.handleRDSResponse() returned err: %v, wantErr: %v", err, test.wantHandleErr)
}
// If the test doesn't expect the callback to be invoked, verify that no
// update or error is pushed to the callback.
//
// Cannot directly compare test.wantUpdate with nil (typed vs non-typed nil:
// https://golang.org/doc/faq#nil_error).
if c := test.wantUpdate; c == nil || (reflect.ValueOf(c).Kind() == reflect.Ptr && reflect.ValueOf(c).IsNil()) {
update, err := gotUpdateCh.Receive()
if err == testutils.ErrRecvTimeout {
return
}
t.Fatalf("Unexpected update: +%v", update)
}
wantUpdate := reflect.ValueOf(test.wantUpdate).Elem().Interface()
uErr, err := gotUpdateCh.Receive()
if err == testutils.ErrRecvTimeout {
t.Fatal("Timeout expecting xDS update")
}
gotUpdate := uErr.(updateErr).u
if diff := cmp.Diff(gotUpdate, wantUpdate); diff != "" {
t.Fatalf("got update : %+v, want %+v, diff: %s", gotUpdate, wantUpdate, diff)
}
gotUpdateErr := uErr.(updateErr).err
if (gotUpdateErr != nil) != test.wantUpdateErr {
t.Fatalf("got xDS update error {%v}, wantErr: %v", gotUpdateErr, test.wantUpdateErr)
}
}
// startServerAndGetCC starts a fake XDS server and also returns a ClientConn
// connected to it.
func startServerAndGetCC(t *testing.T) (*fakeserver.Server, *grpc.ClientConn, func()) {
t.Helper()
fs, sCleanup, err := fakeserver.StartServer()
if err != nil {
t.Fatalf("Failed to start fake xDS server: %v", err)
}
cc, ccCleanup, err := fs.XDSClientConn()
if err != nil {
sCleanup()
t.Fatalf("Failed to get a clientConn to the fake xDS server: %v", err)
}
return fs, cc, func() {
sCleanup()
ccCleanup()
}
}
func newV2Client(p xdsclient.UpdateHandler, cc *grpc.ClientConn, n *basepb.Node, b func(int) time.Duration, l *grpclog.PrefixLogger) (*client, error) {
c, err := newClient(cc, xdsclient.BuildOptions{
Parent: p,
NodeProto: n,
Backoff: b,
Logger: l,
})
if err != nil {
return nil, err
}
return c.(*client), nil
}
// TestV2ClientBackoffAfterRecvError verifies if the v2Client backoffs when it // TestV2ClientBackoffAfterRecvError verifies if the v2Client backoffs when it
// encounters a Recv error while receiving an LDS response. // encounters a Recv error while receiving an LDS response.
func (s) TestV2ClientBackoffAfterRecvError(t *testing.T) { func (s) TestV2ClientBackoffAfterRecvError(t *testing.T) {
@ -381,14 +523,17 @@ func (s) TestV2ClientBackoffAfterRecvError(t *testing.T) {
} }
callbackCh := make(chan struct{}) callbackCh := make(chan struct{})
v2c := newV2Client(&testUpdateReceiver{ v2c, err := newV2Client(&testUpdateReceiver{
f: func(string, map[string]interface{}) { close(callbackCh) }, f: func(string, map[string]interface{}) { close(callbackCh) },
}, cc, goodNodeProto, clientBackoff, nil) }, cc, goodNodeProto, clientBackoff, nil)
defer v2c.close() if err != nil {
t.Fatal(err)
}
defer v2c.Close()
t.Log("Started xds v2Client...") t.Log("Started xds v2Client...")
// v2c.watchLDS(goodLDSTarget1, func(u ldsUpdate, err error) {}) // v2c.watchLDS(goodLDSTarget1, func(u ldsUpdate, err error) {})
v2c.addWatch(ldsURL, goodLDSTarget1) v2c.AddWatch(version.V2ListenerURL, goodLDSTarget1)
if _, err := fakeServer.XDSRequestChan.Receive(); err != nil { if _, err := fakeServer.XDSRequestChan.Receive(); err != nil {
t.Fatalf("Timeout expired when expecting an LDS request") t.Fatalf("Timeout expired when expecting an LDS request")
} }
@ -397,7 +542,7 @@ func (s) TestV2ClientBackoffAfterRecvError(t *testing.T) {
fakeServer.XDSResponseChan <- &fakeserver.Response{Err: errors.New("RPC error")} fakeServer.XDSResponseChan <- &fakeserver.Response{Err: errors.New("RPC error")}
t.Log("Bad LDS response pushed to fakeServer...") t.Log("Bad LDS response pushed to fakeServer...")
timer := time.NewTimer(defaultTestTimeout) timer := time.NewTimer(1 * time.Second)
select { select {
case <-timer.C: case <-timer.C:
t.Fatal("Timeout when expecting LDS update") t.Fatal("Timeout when expecting LDS update")
@ -417,9 +562,9 @@ func (s) TestV2ClientRetriesAfterBrokenStream(t *testing.T) {
defer cleanup() defer cleanup()
callbackCh := testutils.NewChannel() callbackCh := testutils.NewChannel()
v2c := newV2Client(&testUpdateReceiver{ v2c, err := newV2Client(&testUpdateReceiver{
f: func(typeURL string, d map[string]interface{}) { f: func(typeURL string, d map[string]interface{}) {
if typeURL == ldsURL { if typeURL == version.V2ListenerURL {
if u, ok := d[goodLDSTarget1]; ok { if u, ok := d[goodLDSTarget1]; ok {
t.Logf("Received LDS callback with ldsUpdate {%+v}", u) t.Logf("Received LDS callback with ldsUpdate {%+v}", u)
callbackCh.Send(struct{}{}) callbackCh.Send(struct{}{})
@ -427,10 +572,13 @@ func (s) TestV2ClientRetriesAfterBrokenStream(t *testing.T) {
} }
}, },
}, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil) }, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil)
defer v2c.close() if err != nil {
t.Fatal(err)
}
defer v2c.Close()
t.Log("Started xds v2Client...") t.Log("Started xds v2Client...")
v2c.addWatch(ldsURL, goodLDSTarget1) v2c.AddWatch(version.V2ListenerURL, goodLDSTarget1)
if _, err := fakeServer.XDSRequestChan.Receive(); err != nil { if _, err := fakeServer.XDSRequestChan.Receive(); err != nil {
t.Fatalf("Timeout expired when expecting an LDS request") t.Fatalf("Timeout expired when expecting an LDS request")
} }
@ -467,12 +615,6 @@ func (s) TestV2ClientRetriesAfterBrokenStream(t *testing.T) {
// level). And when the stream is re-created, the watcher should get future // level). And when the stream is re-created, the watcher should get future
// updates. // updates.
func (s) TestV2ClientWatchWithoutStream(t *testing.T) { func (s) TestV2ClientWatchWithoutStream(t *testing.T) {
oldWatchExpiryTimeout := defaultWatchExpiryTimeout
defaultWatchExpiryTimeout = 500 * time.Millisecond
defer func() {
defaultWatchExpiryTimeout = oldWatchExpiryTimeout
}()
fakeServer, sCleanup, err := fakeserver.StartServer() fakeServer, sCleanup, err := fakeserver.StartServer()
if err != nil { if err != nil {
t.Fatalf("Failed to start fake xDS server: %v", err) t.Fatalf("Failed to start fake xDS server: %v", err)
@ -490,9 +632,9 @@ func (s) TestV2ClientWatchWithoutStream(t *testing.T) {
defer cc.Close() defer cc.Close()
callbackCh := testutils.NewChannel() callbackCh := testutils.NewChannel()
v2c := newV2Client(&testUpdateReceiver{ v2c, err := newV2Client(&testUpdateReceiver{
f: func(typeURL string, d map[string]interface{}) { f: func(typeURL string, d map[string]interface{}) {
if typeURL == ldsURL { if typeURL == version.V2ListenerURL {
if u, ok := d[goodLDSTarget1]; ok { if u, ok := d[goodLDSTarget1]; ok {
t.Logf("Received LDS callback with ldsUpdate {%+v}", u) t.Logf("Received LDS callback with ldsUpdate {%+v}", u)
callbackCh.Send(u) callbackCh.Send(u)
@ -500,12 +642,15 @@ func (s) TestV2ClientWatchWithoutStream(t *testing.T) {
} }
}, },
}, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil) }, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil)
defer v2c.close() if err != nil {
t.Fatal(err)
}
defer v2c.Close()
t.Log("Started xds v2Client...") t.Log("Started xds v2Client...")
// This watch is started when the xds-ClientConn is in Transient Failure, // This watch is started when the xds-ClientConn is in Transient Failure,
// and no xds stream is created. // and no xds stream is created.
v2c.addWatch(ldsURL, goodLDSTarget1) v2c.AddWatch(version.V2ListenerURL, goodLDSTarget1)
// The watcher should receive an update, with a timeout error in it. // The watcher should receive an update, with a timeout error in it.
if v, err := callbackCh.TimedReceive(100 * time.Millisecond); err == nil { if v, err := callbackCh.TimedReceive(100 * time.Millisecond); err == nil {
@ -528,7 +673,11 @@ func (s) TestV2ClientWatchWithoutStream(t *testing.T) {
if v, err := callbackCh.Receive(); err != nil { if v, err := callbackCh.Receive(); err != nil {
t.Fatal("Timeout when expecting LDS update") t.Fatal("Timeout when expecting LDS update")
} else if _, ok := v.(ldsUpdate); !ok { } else if _, ok := v.(xdsclient.ListenerUpdate); !ok {
t.Fatalf("Expect an LDS update from watcher, got %v", v) t.Fatalf("Expect an LDS update from watcher, got %v", v)
} }
} }
func newStringP(s string) *string {
return &s
}

View File

@ -1,75 +0,0 @@
/*
*
* Copyright 2019 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package client
import (
"fmt"
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
"github.com/golang/protobuf/ptypes"
)
// handleCDSResponse processes an CDS response received from the xDS server. On
// receipt of a good response, it also invokes the registered watcher callback.
func (v2c *v2Client) handleCDSResponse(resp *xdspb.DiscoveryResponse) error {
returnUpdate := make(map[string]ClusterUpdate)
for _, r := range resp.GetResources() {
var resource ptypes.DynamicAny
if err := ptypes.UnmarshalAny(r, &resource); err != nil {
return fmt.Errorf("xds: failed to unmarshal resource in CDS response: %v", err)
}
cluster, ok := resource.Message.(*xdspb.Cluster)
if !ok {
return fmt.Errorf("xds: unexpected resource type: %T in CDS response", resource.Message)
}
v2c.logger.Infof("Resource with name: %v, type: %T, contains: %v", cluster.GetName(), cluster, cluster)
update, err := validateCluster(cluster)
if err != nil {
return err
}
// If the Cluster message in the CDS response did not contain a
// serviceName, we will just use the clusterName for EDS.
if update.ServiceName == "" {
update.ServiceName = cluster.GetName()
}
v2c.logger.Debugf("Resource with name %v, type %T, value %+v added to cache", cluster.GetName(), update, update)
returnUpdate[cluster.GetName()] = update
}
v2c.parent.newCDSUpdate(returnUpdate)
return nil
}
func validateCluster(cluster *xdspb.Cluster) (ClusterUpdate, error) {
emptyUpdate := ClusterUpdate{ServiceName: "", EnableLRS: false}
switch {
case cluster.GetType() != xdspb.Cluster_EDS:
return emptyUpdate, fmt.Errorf("xds: unexpected cluster type %v in response: %+v", cluster.GetType(), cluster)
case cluster.GetEdsClusterConfig().GetEdsConfig().GetAds() == nil:
return emptyUpdate, fmt.Errorf("xds: unexpected edsConfig in response: %+v", cluster)
case cluster.GetLbPolicy() != xdspb.Cluster_ROUND_ROBIN:
return emptyUpdate, fmt.Errorf("xds: unexpected lbPolicy %v in response: %+v", cluster.GetLbPolicy(), cluster)
}
return ClusterUpdate{
ServiceName: cluster.GetEdsClusterConfig().GetServiceName(),
EnableLRS: cluster.GetLrsServer().GetSelf() != nil,
}, nil
}

View File

@ -1,128 +0,0 @@
/*
*
* Copyright 2019 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// All structs/functions in this file should be unexported. They are used in EDS
// balancer tests now, to generate test inputs. Eventually, EDS balancer tests
// should generate EndpointsUpdate directly, instead of generating and parsing the
// proto message.
// TODO: unexported everything in this file.
package client
import (
"fmt"
"net"
"strconv"
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
endpointpb "github.com/envoyproxy/go-control-plane/envoy/api/v2/endpoint"
typepb "github.com/envoyproxy/go-control-plane/envoy/type"
wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
)
// ClusterLoadAssignmentBuilder builds a ClusterLoadAssignment, aka EDS
// response.
type ClusterLoadAssignmentBuilder struct {
v *xdspb.ClusterLoadAssignment
}
// NewClusterLoadAssignmentBuilder creates a ClusterLoadAssignmentBuilder.
func NewClusterLoadAssignmentBuilder(clusterName string, dropPercents []uint32) *ClusterLoadAssignmentBuilder {
var drops []*xdspb.ClusterLoadAssignment_Policy_DropOverload
for i, d := range dropPercents {
drops = append(drops, &xdspb.ClusterLoadAssignment_Policy_DropOverload{
Category: fmt.Sprintf("test-drop-%d", i),
DropPercentage: &typepb.FractionalPercent{
Numerator: d,
Denominator: typepb.FractionalPercent_HUNDRED,
},
})
}
return &ClusterLoadAssignmentBuilder{
v: &xdspb.ClusterLoadAssignment{
ClusterName: clusterName,
Policy: &xdspb.ClusterLoadAssignment_Policy{
DropOverloads: drops,
},
},
}
}
// AddLocalityOptions contains options when adding locality to the builder.
type AddLocalityOptions struct {
Health []corepb.HealthStatus
Weight []uint32
}
// AddLocality adds a locality to the builder.
func (clab *ClusterLoadAssignmentBuilder) AddLocality(subzone string, weight uint32, priority uint32, addrsWithPort []string, opts *AddLocalityOptions) {
var lbEndPoints []*endpointpb.LbEndpoint
for i, a := range addrsWithPort {
host, portStr, err := net.SplitHostPort(a)
if err != nil {
panic("failed to split " + a)
}
port, err := strconv.Atoi(portStr)
if err != nil {
panic("failed to atoi " + portStr)
}
lbe := &endpointpb.LbEndpoint{
HostIdentifier: &endpointpb.LbEndpoint_Endpoint{
Endpoint: &endpointpb.Endpoint{
Address: &corepb.Address{
Address: &corepb.Address_SocketAddress{
SocketAddress: &corepb.SocketAddress{
Protocol: corepb.SocketAddress_TCP,
Address: host,
PortSpecifier: &corepb.SocketAddress_PortValue{
PortValue: uint32(port)}}}}}},
}
if opts != nil {
if i < len(opts.Health) {
lbe.HealthStatus = opts.Health[i]
}
if i < len(opts.Weight) {
lbe.LoadBalancingWeight = &wrapperspb.UInt32Value{Value: opts.Weight[i]}
}
}
lbEndPoints = append(lbEndPoints, lbe)
}
var localityID *corepb.Locality
if subzone != "" {
localityID = &corepb.Locality{
Region: "",
Zone: "",
SubZone: subzone,
}
}
clab.v.Endpoints = append(clab.v.Endpoints, &endpointpb.LocalityLbEndpoints{
Locality: localityID,
LbEndpoints: lbEndPoints,
LoadBalancingWeight: &wrapperspb.UInt32Value{Value: weight},
Priority: priority,
})
}
// Build builds ClusterLoadAssignment.
func (clab *ClusterLoadAssignmentBuilder) Build() *xdspb.ClusterLoadAssignment {
return clab.v
}

View File

@ -1,88 +0,0 @@
/*
*
* Copyright 2019 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package client
import (
"fmt"
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
httppb "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2"
"github.com/golang/protobuf/ptypes"
)
// handleLDSResponse processes an LDS response received from the xDS server. On
// receipt of a good response, it also invokes the registered watcher callback.
func (v2c *v2Client) handleLDSResponse(resp *xdspb.DiscoveryResponse) error {
returnUpdate := make(map[string]ldsUpdate)
for _, r := range resp.GetResources() {
var resource ptypes.DynamicAny
if err := ptypes.UnmarshalAny(r, &resource); err != nil {
return fmt.Errorf("xds: failed to unmarshal resource in LDS response: %v", err)
}
lis, ok := resource.Message.(*xdspb.Listener)
if !ok {
return fmt.Errorf("xds: unexpected resource type: %T in LDS response", resource.Message)
}
v2c.logger.Infof("Resource with name: %v, type: %T, contains: %v", lis.GetName(), lis, lis)
routeName, err := v2c.getRouteConfigNameFromListener(lis)
if err != nil {
return err
}
returnUpdate[lis.GetName()] = ldsUpdate{routeName: routeName}
}
v2c.parent.newLDSUpdate(returnUpdate)
return nil
}
// getRouteConfigNameFromListener checks if the provided Listener proto meets
// the expected criteria. If so, it returns a non-empty routeConfigName.
func (v2c *v2Client) getRouteConfigNameFromListener(lis *xdspb.Listener) (string, error) {
if lis.GetApiListener() == nil {
return "", fmt.Errorf("xds: no api_listener field in LDS response %+v", lis)
}
var apiAny ptypes.DynamicAny
if err := ptypes.UnmarshalAny(lis.GetApiListener().GetApiListener(), &apiAny); err != nil {
return "", fmt.Errorf("xds: failed to unmarshal api_listner in LDS response: %v", err)
}
apiLis, ok := apiAny.Message.(*httppb.HttpConnectionManager)
if !ok {
return "", fmt.Errorf("xds: unexpected api_listener type: %T in LDS response", apiAny.Message)
}
v2c.logger.Infof("Resource with type %T, contains %v", apiLis, apiLis)
switch apiLis.RouteSpecifier.(type) {
case *httppb.HttpConnectionManager_Rds:
if apiLis.GetRds().GetConfigSource().GetAds() == nil {
return "", fmt.Errorf("xds: ConfigSource is not ADS in LDS response: %+v", lis)
}
name := apiLis.GetRds().GetRouteConfigName()
if name == "" {
return "", fmt.Errorf("xds: empty route_config_name in LDS response: %+v", lis)
}
return name, nil
case *httppb.HttpConnectionManager_RouteConfig:
// TODO: Add support for specifying the RouteConfiguration inline
// in the LDS response.
return "", fmt.Errorf("xds: LDS response contains RDS config inline. Not supported for now: %+v", apiLis)
case nil:
return "", fmt.Errorf("xds: no RouteSpecifier in received LDS response: %+v", apiLis)
default:
return "", fmt.Errorf("xds: unsupported type %T for RouteSpecifier in received LDS response", apiLis.RouteSpecifier)
}
}

View File

@ -1,323 +0,0 @@
/*
*
* Copyright 2019 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package client
import (
"fmt"
"strings"
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
routepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
typepb "github.com/envoyproxy/go-control-plane/envoy/type"
"github.com/golang/protobuf/ptypes"
"google.golang.org/grpc/internal/grpclog"
)
// handleRDSResponse processes an RDS response received from the xDS server. On
// receipt of a good response, it caches validated resources and also invokes
// the registered watcher callback.
func (v2c *v2Client) handleRDSResponse(resp *xdspb.DiscoveryResponse) error {
v2c.mu.Lock()
hostname := v2c.hostname
v2c.mu.Unlock()
returnUpdate := make(map[string]rdsUpdate)
for _, r := range resp.GetResources() {
var resource ptypes.DynamicAny
if err := ptypes.UnmarshalAny(r, &resource); err != nil {
return fmt.Errorf("xds: failed to unmarshal resource in RDS response: %v", err)
}
rc, ok := resource.Message.(*xdspb.RouteConfiguration)
if !ok {
return fmt.Errorf("xds: unexpected resource type: %T in RDS response", resource.Message)
}
v2c.logger.Infof("Resource with name: %v, type: %T, contains: %v. Picking routes for current watching hostname %v", rc.GetName(), rc, rc, v2c.hostname)
// Use the hostname (resourceName for LDS) to find the routes.
u, err := generateRDSUpdateFromRouteConfiguration(rc, hostname, v2c.logger)
if err != nil {
return fmt.Errorf("xds: received invalid RouteConfiguration in RDS response: %+v with err: %v", rc, err)
}
// If we get here, it means that this resource was a good one.
returnUpdate[rc.GetName()] = u
}
v2c.parent.newRDSUpdate(returnUpdate)
return nil
}
// generateRDSUpdateFromRouteConfiguration checks if the provided
// RouteConfiguration meets the expected criteria. If so, it returns a rdsUpdate
// with nil error.
//
// A RouteConfiguration resource is considered valid when only if it contains a
// VirtualHost whose domain field matches the server name from the URI passed
// to the gRPC channel, and it contains a clusterName or a weighted cluster.
//
// The RouteConfiguration includes a list of VirtualHosts, which may have zero
// or more elements. We are interested in the element whose domains field
// matches the server name specified in the "xds:" URI. The only field in the
// VirtualHost proto that the we are interested in is the list of routes. We
// only look at the last route in the list (the default route), whose match
// field must be empty and whose route field must be set. Inside that route
// message, the cluster field will contain the clusterName or weighted clusters
// we are looking for.
func generateRDSUpdateFromRouteConfiguration(rc *xdspb.RouteConfiguration, host string, logger *grpclog.PrefixLogger) (rdsUpdate, error) {
//
// Currently this returns "" on error, and the caller will return an error.
// But the error doesn't contain details of why the response is invalid
// (mismatch domain or empty route).
//
// For logging purposes, we can log in line. But if we want to populate
// error details for nack, a detailed error needs to be returned.
vh := findBestMatchingVirtualHost(host, rc.GetVirtualHosts())
if vh == nil {
// No matching virtual host found.
return rdsUpdate{}, fmt.Errorf("no matching virtual host found")
}
if len(vh.Routes) == 0 {
// The matched virtual host has no routes, this is invalid because there
// should be at least one default route.
return rdsUpdate{}, fmt.Errorf("matched virtual host has no routes")
}
routes, err := routesProtoToSlice(vh.Routes, logger)
if err != nil {
return rdsUpdate{}, fmt.Errorf("received route is invalid: %v", err)
}
return rdsUpdate{routes: routes}, nil
}
func routesProtoToSlice(routes []*routepb.Route, logger *grpclog.PrefixLogger) ([]*Route, error) {
var routesRet []*Route
for _, r := range routes {
match := r.GetMatch()
if match == nil {
return nil, fmt.Errorf("route %+v doesn't have a match", r)
}
if len(match.GetQueryParameters()) != 0 {
// Ignore route with query parameters.
logger.Warningf("route %+v has query parameter matchers, the route will be ignored", r)
continue
}
if caseSensitive := match.GetCaseSensitive(); caseSensitive != nil && !caseSensitive.Value {
return nil, fmt.Errorf("route %+v has case-sensitive false", r)
}
pathSp := match.GetPathSpecifier()
if pathSp == nil {
return nil, fmt.Errorf("route %+v doesn't have a path specifier", r)
}
var route Route
switch pt := pathSp.(type) {
case *routepb.RouteMatch_Prefix:
route.Prefix = &pt.Prefix
case *routepb.RouteMatch_Path:
route.Path = &pt.Path
case *routepb.RouteMatch_SafeRegex:
route.Regex = &pt.SafeRegex.Regex
case *routepb.RouteMatch_Regex:
return nil, fmt.Errorf("route %+v has Regex, expected SafeRegex instead", r)
default:
logger.Warningf("route %+v has an unrecognized path specifier: %+v", r, pt)
continue
}
for _, h := range match.GetHeaders() {
var header HeaderMatcher
switch ht := h.GetHeaderMatchSpecifier().(type) {
case *routepb.HeaderMatcher_ExactMatch:
header.ExactMatch = &ht.ExactMatch
case *routepb.HeaderMatcher_SafeRegexMatch:
header.RegexMatch = &ht.SafeRegexMatch.Regex
case *routepb.HeaderMatcher_RangeMatch:
header.RangeMatch = &Int64Range{
Start: ht.RangeMatch.Start,
End: ht.RangeMatch.End,
}
case *routepb.HeaderMatcher_PresentMatch:
header.PresentMatch = &ht.PresentMatch
case *routepb.HeaderMatcher_PrefixMatch:
header.PrefixMatch = &ht.PrefixMatch
case *routepb.HeaderMatcher_SuffixMatch:
header.SuffixMatch = &ht.SuffixMatch
case *routepb.HeaderMatcher_RegexMatch:
return nil, fmt.Errorf("route %+v has a header matcher with Regex, expected SafeRegex instead", r)
default:
logger.Warningf("route %+v has an unrecognized header matcher: %+v", r, ht)
continue
}
header.Name = h.GetName()
invert := h.GetInvertMatch()
header.InvertMatch = &invert
route.Headers = append(route.Headers, &header)
}
if fr := match.GetRuntimeFraction(); fr != nil {
d := fr.GetDefaultValue()
n := d.GetNumerator()
switch d.GetDenominator() {
case typepb.FractionalPercent_HUNDRED:
n *= 10000
case typepb.FractionalPercent_TEN_THOUSAND:
n *= 100
case typepb.FractionalPercent_MILLION:
}
route.Fraction = &n
}
clusters := make(map[string]uint32)
switch a := r.GetRoute().GetClusterSpecifier().(type) {
case *routepb.RouteAction_Cluster:
clusters[a.Cluster] = 1
case *routepb.RouteAction_WeightedClusters:
wcs := a.WeightedClusters
var totalWeight uint32
for _, c := range wcs.Clusters {
w := c.GetWeight().GetValue()
clusters[c.GetName()] = w
totalWeight += w
}
if totalWeight != wcs.GetTotalWeight().GetValue() {
return nil, fmt.Errorf("route %+v, action %+v, weights of clusters do not add up to total total weight, got: %v, want %v", r, a, wcs.GetTotalWeight().GetValue(), totalWeight)
}
case *routepb.RouteAction_ClusterHeader:
continue
}
route.Action = clusters
routesRet = append(routesRet, &route)
}
return routesRet, nil
}
func weightedClustersProtoToMap(wc *routepb.WeightedCluster) (map[string]uint32, error) {
ret := make(map[string]uint32)
var totalWeight uint32 = 100
if t := wc.GetTotalWeight().GetValue(); t != 0 {
totalWeight = t
}
for _, cw := range wc.Clusters {
w := cw.Weight.GetValue()
ret[cw.Name] = w
totalWeight -= w
}
if totalWeight != 0 {
return nil, fmt.Errorf("weights of clusters do not add up to total total weight, difference: %v", totalWeight)
}
return ret, nil
}
type domainMatchType int
const (
domainMatchTypeInvalid domainMatchType = iota
domainMatchTypeUniversal
domainMatchTypePrefix
domainMatchTypeSuffix
domainMatchTypeExact
)
// Exact > Suffix > Prefix > Universal > Invalid.
func (t domainMatchType) betterThan(b domainMatchType) bool {
return t > b
}
func matchTypeForDomain(d string) domainMatchType {
if d == "" {
return domainMatchTypeInvalid
}
if d == "*" {
return domainMatchTypeUniversal
}
if strings.HasPrefix(d, "*") {
return domainMatchTypeSuffix
}
if strings.HasSuffix(d, "*") {
return domainMatchTypePrefix
}
if strings.Contains(d, "*") {
return domainMatchTypeInvalid
}
return domainMatchTypeExact
}
func match(domain, host string) (domainMatchType, bool) {
switch typ := matchTypeForDomain(domain); typ {
case domainMatchTypeInvalid:
return typ, false
case domainMatchTypeUniversal:
return typ, true
case domainMatchTypePrefix:
// abc.*
return typ, strings.HasPrefix(host, strings.TrimSuffix(domain, "*"))
case domainMatchTypeSuffix:
// *.123
return typ, strings.HasSuffix(host, strings.TrimPrefix(domain, "*"))
case domainMatchTypeExact:
return typ, domain == host
default:
return domainMatchTypeInvalid, false
}
}
// findBestMatchingVirtualHost returns the virtual host whose domains field best
// matches host
//
// The domains field support 4 different matching pattern types:
// - Exact match
// - Suffix match (e.g. “*ABC”)
// - Prefix match (e.g. “ABC*)
// - Universal match (e.g. “*”)
//
// The best match is defined as:
// - A match is better if its matching pattern type is better
// - Exact match > suffix match > prefix match > universal match
// - If two matches are of the same pattern type, the longer match is better
// - This is to compare the length of the matching pattern, e.g. “*ABCDE” >
// “*ABC”
func findBestMatchingVirtualHost(host string, vHosts []*routepb.VirtualHost) *routepb.VirtualHost {
var (
matchVh *routepb.VirtualHost
matchType = domainMatchTypeInvalid
matchLen int
)
for _, vh := range vHosts {
for _, domain := range vh.GetDomains() {
typ, matched := match(domain, host)
if typ == domainMatchTypeInvalid {
// The rds response is invalid.
return nil
}
if matchType.betterThan(typ) || matchType == typ && matchLen >= len(domain) || !matched {
// The previous match has better type, or the previous match has
// better length, or this domain isn't a match.
continue
}
matchVh = vh
matchType = typ
matchLen = len(domain)
}
}
return matchVh
}

View File

@ -1,699 +0,0 @@
/*
*
* Copyright 2019 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package client
import (
"testing"
"time"
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
routepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
typepb "github.com/envoyproxy/go-control-plane/envoy/type"
"github.com/golang/protobuf/proto"
wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/grpc/xds/internal/testutils/fakeserver"
)
func (s) TestRDSGenerateRDSUpdateFromRouteConfiguration(t *testing.T) {
tests := []struct {
name string
rc *xdspb.RouteConfiguration
wantUpdate rdsUpdate
wantError bool
}{
{
name: "no-virtual-hosts-in-rc",
rc: emptyRouteConfig,
wantError: true,
},
{
name: "no-domains-in-rc",
rc: noDomainsInRouteConfig,
wantError: true,
},
{
name: "non-matching-domain-in-rc",
rc: &xdspb.RouteConfiguration{
VirtualHosts: []*routepb.VirtualHost{
{Domains: []string{uninterestingDomain}},
},
},
wantError: true,
},
{
name: "no-routes-in-rc",
rc: &xdspb.RouteConfiguration{
VirtualHosts: []*routepb.VirtualHost{
{Domains: []string{goodLDSTarget1}},
},
},
wantError: true,
},
{
name: "default-route-match-field-is-nil",
rc: &xdspb.RouteConfiguration{
VirtualHosts: []*routepb.VirtualHost{
{
Domains: []string{goodLDSTarget1},
Routes: []*routepb.Route{
{
Action: &routepb.Route_Route{
Route: &routepb.RouteAction{
ClusterSpecifier: &routepb.RouteAction_Cluster{Cluster: goodClusterName1},
},
},
},
},
},
},
},
wantError: true,
},
{
name: "default-route-match-field-is-non-nil",
rc: &xdspb.RouteConfiguration{
VirtualHosts: []*routepb.VirtualHost{
{
Domains: []string{goodLDSTarget1},
Routes: []*routepb.Route{
{
Match: &routepb.RouteMatch{},
Action: &routepb.Route_Route{},
},
},
},
},
},
wantError: true,
},
{
name: "default-route-routeaction-field-is-nil",
rc: &xdspb.RouteConfiguration{
VirtualHosts: []*routepb.VirtualHost{
{
Domains: []string{goodLDSTarget1},
Routes: []*routepb.Route{{}},
},
},
},
wantError: true,
},
{
name: "default-route-cluster-field-is-empty",
rc: &xdspb.RouteConfiguration{
VirtualHosts: []*routepb.VirtualHost{
{
Domains: []string{goodLDSTarget1},
Routes: []*routepb.Route{
{
Action: &routepb.Route_Route{
Route: &routepb.RouteAction{
ClusterSpecifier: &routepb.RouteAction_ClusterHeader{},
},
},
},
},
},
},
},
wantError: true,
},
{
// default route's match sets case-sensitive to false.
name: "good-route-config-but-with-casesensitive-false",
rc: &xdspb.RouteConfiguration{
Name: goodRouteName1,
VirtualHosts: []*routepb.VirtualHost{{
Domains: []string{goodLDSTarget1},
Routes: []*routepb.Route{{
Match: &routepb.RouteMatch{
PathSpecifier: &routepb.RouteMatch_Prefix{Prefix: "/"},
CaseSensitive: &wrapperspb.BoolValue{Value: false},
},
Action: &routepb.Route_Route{
Route: &routepb.RouteAction{
ClusterSpecifier: &routepb.RouteAction_Cluster{Cluster: goodClusterName1},
}}}}}}},
wantError: true,
},
{
name: "good-route-config-with-empty-string-route",
rc: goodRouteConfig1,
wantUpdate: rdsUpdate{routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{goodClusterName1: 1}}}},
},
{
// default route's match is not empty string, but "/".
name: "good-route-config-with-slash-string-route",
rc: &xdspb.RouteConfiguration{
Name: goodRouteName1,
VirtualHosts: []*routepb.VirtualHost{{
Domains: []string{goodLDSTarget1},
Routes: []*routepb.Route{{
Match: &routepb.RouteMatch{PathSpecifier: &routepb.RouteMatch_Prefix{Prefix: "/"}},
Action: &routepb.Route_Route{
Route: &routepb.RouteAction{
ClusterSpecifier: &routepb.RouteAction_Cluster{Cluster: goodClusterName1},
}}}}}}},
wantUpdate: rdsUpdate{routes: []*Route{{Prefix: newStringP("/"), Action: map[string]uint32{goodClusterName1: 1}}}},
},
{
// weights not add up to total-weight.
name: "route-config-with-weighted_clusters_weights_not_add_up",
rc: &xdspb.RouteConfiguration{
Name: goodRouteName1,
VirtualHosts: []*routepb.VirtualHost{{
Domains: []string{goodLDSTarget1},
Routes: []*routepb.Route{{
Match: &routepb.RouteMatch{PathSpecifier: &routepb.RouteMatch_Prefix{Prefix: "/"}},
Action: &routepb.Route_Route{
Route: &routepb.RouteAction{
ClusterSpecifier: &routepb.RouteAction_WeightedClusters{
WeightedClusters: &routepb.WeightedCluster{
Clusters: []*routepb.WeightedCluster_ClusterWeight{
{Name: "a", Weight: &wrapperspb.UInt32Value{Value: 2}},
{Name: "b", Weight: &wrapperspb.UInt32Value{Value: 3}},
{Name: "c", Weight: &wrapperspb.UInt32Value{Value: 5}},
},
TotalWeight: &wrapperspb.UInt32Value{Value: 30},
}}}}}}}}},
wantError: true,
},
{
name: "good-route-config-with-weighted_clusters",
rc: &xdspb.RouteConfiguration{
Name: goodRouteName1,
VirtualHosts: []*routepb.VirtualHost{{
Domains: []string{goodLDSTarget1},
Routes: []*routepb.Route{{
Match: &routepb.RouteMatch{PathSpecifier: &routepb.RouteMatch_Prefix{Prefix: "/"}},
Action: &routepb.Route_Route{
Route: &routepb.RouteAction{
ClusterSpecifier: &routepb.RouteAction_WeightedClusters{
WeightedClusters: &routepb.WeightedCluster{
Clusters: []*routepb.WeightedCluster_ClusterWeight{
{Name: "a", Weight: &wrapperspb.UInt32Value{Value: 2}},
{Name: "b", Weight: &wrapperspb.UInt32Value{Value: 3}},
{Name: "c", Weight: &wrapperspb.UInt32Value{Value: 5}},
},
TotalWeight: &wrapperspb.UInt32Value{Value: 10},
}}}}}}}}},
wantUpdate: rdsUpdate{routes: []*Route{{Prefix: newStringP("/"), Action: map[string]uint32{"a": 2, "b": 3, "c": 5}}}},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
gotUpdate, gotError := generateRDSUpdateFromRouteConfiguration(test.rc, goodLDSTarget1, nil)
if !cmp.Equal(gotUpdate, test.wantUpdate, cmp.AllowUnexported(rdsUpdate{})) || (gotError != nil) != test.wantError {
t.Errorf("generateRDSUpdateFromRouteConfiguration(%+v, %v) = %v, want %v", test.rc, goodLDSTarget1, gotUpdate, test.wantUpdate)
}
})
}
}
// doLDS makes a LDS watch, and waits for the response and ack to finish.
//
// This is called by RDS tests to start LDS first, because LDS is a
// pre-requirement for RDS, and RDS handle would fail without an existing LDS
// watch.
func doLDS(t *testing.T, v2c *v2Client, fakeServer *fakeserver.Server) {
v2c.addWatch(ldsURL, goodLDSTarget1)
if _, err := fakeServer.XDSRequestChan.Receive(); err != nil {
t.Fatalf("Timeout waiting for LDS request: %v", err)
}
}
// TestRDSHandleResponseWithRouting starts a fake xDS server, makes a ClientConn
// to it, and creates a v2Client using it. Then, it registers an LDS and RDS
// watcher and tests different RDS responses.
func (s) TestRDSHandleResponseWithRouting(t *testing.T) {
tests := []struct {
name string
rdsResponse *xdspb.DiscoveryResponse
wantErr bool
wantUpdate *rdsUpdate
wantUpdateErr bool
}{
// Badly marshaled RDS response.
{
name: "badly-marshaled-response",
rdsResponse: badlyMarshaledRDSResponse,
wantErr: true,
wantUpdate: nil,
wantUpdateErr: false,
},
// Response does not contain RouteConfiguration proto.
{
name: "no-route-config-in-response",
rdsResponse: badResourceTypeInRDSResponse,
wantErr: true,
wantUpdate: nil,
wantUpdateErr: false,
},
// No VirtualHosts in the response. Just one test case here for a bad
// RouteConfiguration, since the others are covered in
// TestGetClusterFromRouteConfiguration.
{
name: "no-virtual-hosts-in-response",
rdsResponse: noVirtualHostsInRDSResponse,
wantErr: true,
wantUpdate: nil,
wantUpdateErr: false,
},
// Response contains one good RouteConfiguration, uninteresting though.
{
name: "one-uninteresting-route-config",
rdsResponse: goodRDSResponse2,
wantErr: false,
wantUpdate: nil,
wantUpdateErr: false,
},
// Response contains one good interesting RouteConfiguration.
{
name: "one-good-route-config",
rdsResponse: goodRDSResponse1,
wantErr: false,
wantUpdate: &rdsUpdate{routes: []*Route{{Prefix: newStringP(""), Action: map[string]uint32{goodClusterName1: 1}}}},
wantUpdateErr: false,
},
{
name: "one-good-route-config with routes",
rdsResponse: goodRDSResponse1,
wantErr: false,
wantUpdate: &rdsUpdate{
// Instead of just weighted targets when routing is disabled,
// this result contains a route with perfix "", and action as
// weighted targets.
routes: []*Route{{
Prefix: newStringP(""),
Action: map[string]uint32{goodClusterName1: 1},
}},
},
wantUpdateErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testWatchHandle(t, &watchHandleTestcase{
typeURL: rdsURL,
resourceName: goodRouteName1,
responseToHandle: test.rdsResponse,
wantHandleErr: test.wantErr,
wantUpdate: test.wantUpdate,
wantUpdateErr: test.wantUpdateErr,
})
})
}
}
// TestRDSHandleResponseWithoutLDSWatch tests the case where the v2Client
// receives an RDS response without a registered LDS watcher.
func (s) TestRDSHandleResponseWithoutLDSWatch(t *testing.T) {
_, cc, cleanup := startServerAndGetCC(t)
defer cleanup()
v2c := newV2Client(&testUpdateReceiver{
f: func(string, map[string]interface{}) {},
}, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil)
defer v2c.close()
if v2c.handleRDSResponse(goodRDSResponse1) == nil {
t.Fatal("v2c.handleRDSResponse() succeeded, should have failed")
}
}
// TestRDSHandleResponseWithoutRDSWatch tests the case where the v2Client
// receives an RDS response without a registered RDS watcher.
func (s) TestRDSHandleResponseWithoutRDSWatch(t *testing.T) {
fakeServer, cc, cleanup := startServerAndGetCC(t)
defer cleanup()
v2c := newV2Client(&testUpdateReceiver{
f: func(string, map[string]interface{}) {},
}, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil)
defer v2c.close()
doLDS(t, v2c, fakeServer)
if v2c.handleRDSResponse(badResourceTypeInRDSResponse) == nil {
t.Fatal("v2c.handleRDSResponse() succeeded, should have failed")
}
if v2c.handleRDSResponse(goodRDSResponse1) != nil {
t.Fatal("v2c.handleRDSResponse() succeeded, should have failed")
}
}
func (s) TestMatchTypeForDomain(t *testing.T) {
tests := []struct {
d string
want domainMatchType
}{
{d: "", want: domainMatchTypeInvalid},
{d: "*", want: domainMatchTypeUniversal},
{d: "bar.*", want: domainMatchTypePrefix},
{d: "*.abc.com", want: domainMatchTypeSuffix},
{d: "foo.bar.com", want: domainMatchTypeExact},
{d: "foo.*.com", want: domainMatchTypeInvalid},
}
for _, tt := range tests {
if got := matchTypeForDomain(tt.d); got != tt.want {
t.Errorf("matchTypeForDomain(%q) = %v, want %v", tt.d, got, tt.want)
}
}
}
func (s) TestMatch(t *testing.T) {
tests := []struct {
name string
domain string
host string
wantTyp domainMatchType
wantMatched bool
}{
{name: "invalid-empty", domain: "", host: "", wantTyp: domainMatchTypeInvalid, wantMatched: false},
{name: "invalid", domain: "a.*.b", host: "", wantTyp: domainMatchTypeInvalid, wantMatched: false},
{name: "universal", domain: "*", host: "abc.com", wantTyp: domainMatchTypeUniversal, wantMatched: true},
{name: "prefix-match", domain: "abc.*", host: "abc.123", wantTyp: domainMatchTypePrefix, wantMatched: true},
{name: "prefix-no-match", domain: "abc.*", host: "abcd.123", wantTyp: domainMatchTypePrefix, wantMatched: false},
{name: "suffix-match", domain: "*.123", host: "abc.123", wantTyp: domainMatchTypeSuffix, wantMatched: true},
{name: "suffix-no-match", domain: "*.123", host: "abc.1234", wantTyp: domainMatchTypeSuffix, wantMatched: false},
{name: "exact-match", domain: "foo.bar", host: "foo.bar", wantTyp: domainMatchTypeExact, wantMatched: true},
{name: "exact-no-match", domain: "foo.bar.com", host: "foo.bar", wantTyp: domainMatchTypeExact, wantMatched: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if gotTyp, gotMatched := match(tt.domain, tt.host); gotTyp != tt.wantTyp || gotMatched != tt.wantMatched {
t.Errorf("match() = %v, %v, want %v, %v", gotTyp, gotMatched, tt.wantTyp, tt.wantMatched)
}
})
}
}
func (s) TestFindBestMatchingVirtualHost(t *testing.T) {
var (
oneExactMatch = &routepb.VirtualHost{
Name: "one-exact-match",
Domains: []string{"foo.bar.com"},
}
oneSuffixMatch = &routepb.VirtualHost{
Name: "one-suffix-match",
Domains: []string{"*.bar.com"},
}
onePrefixMatch = &routepb.VirtualHost{
Name: "one-prefix-match",
Domains: []string{"foo.bar.*"},
}
oneUniversalMatch = &routepb.VirtualHost{
Name: "one-universal-match",
Domains: []string{"*"},
}
longExactMatch = &routepb.VirtualHost{
Name: "one-exact-match",
Domains: []string{"v2.foo.bar.com"},
}
multipleMatch = &routepb.VirtualHost{
Name: "multiple-match",
Domains: []string{"pi.foo.bar.com", "314.*", "*.159"},
}
vhs = []*routepb.VirtualHost{oneExactMatch, oneSuffixMatch, onePrefixMatch, oneUniversalMatch, longExactMatch, multipleMatch}
)
tests := []struct {
name string
host string
vHosts []*routepb.VirtualHost
want *routepb.VirtualHost
}{
{name: "exact-match", host: "foo.bar.com", vHosts: vhs, want: oneExactMatch},
{name: "suffix-match", host: "123.bar.com", vHosts: vhs, want: oneSuffixMatch},
{name: "prefix-match", host: "foo.bar.org", vHosts: vhs, want: onePrefixMatch},
{name: "universal-match", host: "abc.123", vHosts: vhs, want: oneUniversalMatch},
{name: "long-exact-match", host: "v2.foo.bar.com", vHosts: vhs, want: longExactMatch},
// Matches suffix "*.bar.com" and exact "pi.foo.bar.com". Takes exact.
{name: "multiple-match-exact", host: "pi.foo.bar.com", vHosts: vhs, want: multipleMatch},
// Matches suffix "*.159" and prefix "foo.bar.*". Takes suffix.
{name: "multiple-match-suffix", host: "foo.bar.159", vHosts: vhs, want: multipleMatch},
// Matches suffix "*.bar.com" and prefix "314.*". Takes suffix.
{name: "multiple-match-prefix", host: "314.bar.com", vHosts: vhs, want: oneSuffixMatch},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := findBestMatchingVirtualHost(tt.host, tt.vHosts); !cmp.Equal(got, tt.want, cmp.Comparer(proto.Equal)) {
t.Errorf("findBestMatchingVirtualHost() = %v, want %v", got, tt.want)
}
})
}
}
func (s) TestWeightedClustersProtoToMap(t *testing.T) {
tests := []struct {
name string
wc *routepb.WeightedCluster
want map[string]uint32
wantErr bool
}{
{
name: "weight not add up to non default total",
wc: &routepb.WeightedCluster{
Clusters: []*routepb.WeightedCluster_ClusterWeight{
{Name: "a", Weight: &wrapperspb.UInt32Value{Value: 1}},
{Name: "b", Weight: &wrapperspb.UInt32Value{Value: 1}},
{Name: "c", Weight: &wrapperspb.UInt32Value{Value: 1}},
},
TotalWeight: &wrapperspb.UInt32Value{Value: 10},
},
wantErr: true,
},
{
name: "weight not add up to default total",
wc: &routepb.WeightedCluster{
Clusters: []*routepb.WeightedCluster_ClusterWeight{
{Name: "a", Weight: &wrapperspb.UInt32Value{Value: 2}},
{Name: "b", Weight: &wrapperspb.UInt32Value{Value: 3}},
{Name: "c", Weight: &wrapperspb.UInt32Value{Value: 5}},
},
TotalWeight: nil,
},
wantErr: true,
},
{
name: "ok non default total weight",
wc: &routepb.WeightedCluster{
Clusters: []*routepb.WeightedCluster_ClusterWeight{
{Name: "a", Weight: &wrapperspb.UInt32Value{Value: 2}},
{Name: "b", Weight: &wrapperspb.UInt32Value{Value: 3}},
{Name: "c", Weight: &wrapperspb.UInt32Value{Value: 5}},
},
TotalWeight: &wrapperspb.UInt32Value{Value: 10},
},
want: map[string]uint32{"a": 2, "b": 3, "c": 5},
},
{
name: "ok default total weight is 100",
wc: &routepb.WeightedCluster{
Clusters: []*routepb.WeightedCluster_ClusterWeight{
{Name: "a", Weight: &wrapperspb.UInt32Value{Value: 20}},
{Name: "b", Weight: &wrapperspb.UInt32Value{Value: 30}},
{Name: "c", Weight: &wrapperspb.UInt32Value{Value: 50}},
},
TotalWeight: nil,
},
want: map[string]uint32{"a": 20, "b": 30, "c": 50},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := weightedClustersProtoToMap(tt.wc)
if (err != nil) != tt.wantErr {
t.Errorf("weightedClustersProtoToMap() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !cmp.Equal(got, tt.want) {
t.Errorf("weightedClustersProtoToMap() got = %v, want %v", got, tt.want)
}
})
}
}
func TestRoutesProtoToSlice(t *testing.T) {
tests := []struct {
name string
routes []*routepb.Route
wantRoutes []*Route
wantErr bool
}{
{
name: "no path",
routes: []*routepb.Route{{
Match: &routepb.RouteMatch{},
}},
wantErr: true,
},
{
name: "path is regex instead of saferegex",
routes: []*routepb.Route{{
Match: &routepb.RouteMatch{
PathSpecifier: &routepb.RouteMatch_Regex{Regex: "*"},
},
}},
wantErr: true,
},
{
name: "header contains regex",
routes: []*routepb.Route{{
Match: &routepb.RouteMatch{
PathSpecifier: &routepb.RouteMatch_Prefix{Prefix: "/"},
Headers: []*routepb.HeaderMatcher{{
Name: "th",
HeaderMatchSpecifier: &routepb.HeaderMatcher_RegexMatch{
RegexMatch: "*",
},
}},
},
}},
wantErr: true,
},
{
name: "case_sensitive is false",
routes: []*routepb.Route{{
Match: &routepb.RouteMatch{
PathSpecifier: &routepb.RouteMatch_Prefix{Prefix: "/"},
CaseSensitive: &wrapperspb.BoolValue{Value: false},
},
}},
wantErr: true,
},
{
name: "good",
routes: []*routepb.Route{
{
Match: &routepb.RouteMatch{
PathSpecifier: &routepb.RouteMatch_Prefix{Prefix: "/a/"},
Headers: []*routepb.HeaderMatcher{
{
Name: "th",
HeaderMatchSpecifier: &routepb.HeaderMatcher_PrefixMatch{
PrefixMatch: "tv",
},
InvertMatch: true,
},
},
RuntimeFraction: &corepb.RuntimeFractionalPercent{
DefaultValue: &typepb.FractionalPercent{
Numerator: 1,
Denominator: typepb.FractionalPercent_HUNDRED,
},
},
},
Action: &routepb.Route_Route{
Route: &routepb.RouteAction{
ClusterSpecifier: &routepb.RouteAction_WeightedClusters{
WeightedClusters: &routepb.WeightedCluster{
Clusters: []*routepb.WeightedCluster_ClusterWeight{
{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
},
TotalWeight: &wrapperspb.UInt32Value{Value: 100},
}}}},
},
},
wantRoutes: []*Route{{
Prefix: newStringP("/a/"),
Headers: []*HeaderMatcher{
{
Name: "th",
InvertMatch: newBoolP(true),
PrefixMatch: newStringP("tv"),
},
},
Fraction: newUInt32P(10000),
Action: map[string]uint32{"A": 40, "B": 60},
}},
wantErr: false,
},
{
name: "query is ignored",
routes: []*routepb.Route{
{
Match: &routepb.RouteMatch{
PathSpecifier: &routepb.RouteMatch_Prefix{Prefix: "/a/"},
},
Action: &routepb.Route_Route{
Route: &routepb.RouteAction{
ClusterSpecifier: &routepb.RouteAction_WeightedClusters{
WeightedClusters: &routepb.WeightedCluster{
Clusters: []*routepb.WeightedCluster_ClusterWeight{
{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
},
TotalWeight: &wrapperspb.UInt32Value{Value: 100},
}}}},
},
{
Name: "with_query",
Match: &routepb.RouteMatch{
PathSpecifier: &routepb.RouteMatch_Prefix{Prefix: "/b/"},
QueryParameters: []*routepb.QueryParameterMatcher{{Name: "route_will_be_ignored"}},
},
},
},
// Only one route in the result, because the second one with query
// parameters is ignored.
wantRoutes: []*Route{{
Prefix: newStringP("/a/"),
Action: map[string]uint32{"A": 40, "B": 60},
}},
wantErr: false,
},
}
cmpOpts := []cmp.Option{
cmp.AllowUnexported(Route{}, HeaderMatcher{}, Int64Range{}),
cmpopts.EquateEmpty(),
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := routesProtoToSlice(tt.routes, nil)
if (err != nil) != tt.wantErr {
t.Errorf("routesProtoToSlice() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !cmp.Equal(got, tt.wantRoutes, cmpOpts...) {
t.Errorf("routesProtoToSlice() got = %v, want %v, diff: %v", got, tt.wantRoutes, cmp.Diff(got, tt.wantRoutes, cmpOpts...))
}
})
}
}
func newStringP(s string) *string {
return &s
}
func newUInt32P(i uint32) *uint32 {
return &i
}
func newBoolP(b bool) *bool {
return &b
}

View File

@ -27,7 +27,6 @@ import (
"google.golang.org/grpc/serviceconfig" "google.golang.org/grpc/serviceconfig"
_ "google.golang.org/grpc/xds/internal/balancer/weightedtarget" _ "google.golang.org/grpc/xds/internal/balancer/weightedtarget"
_ "google.golang.org/grpc/xds/internal/balancer/xdsrouting" _ "google.golang.org/grpc/xds/internal/balancer/xdsrouting"
"google.golang.org/grpc/xds/internal/client"
xdsclient "google.golang.org/grpc/xds/internal/client" xdsclient "google.golang.org/grpc/xds/internal/client"
) )
@ -312,13 +311,13 @@ func TestRoutesToJSON(t *testing.T) {
func TestServiceUpdateToJSON(t *testing.T) { func TestServiceUpdateToJSON(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
su client.ServiceUpdate su xdsclient.ServiceUpdate
wantJSON string wantJSON string
wantErr bool wantErr bool
}{ }{
{ {
name: "routing", name: "routing",
su: client.ServiceUpdate{ su: xdsclient.ServiceUpdate{
Routes: []*xdsclient.Route{{ Routes: []*xdsclient.Route{{
Path: newStringP("/service_1/method_1"), Path: newStringP("/service_1/method_1"),
Action: map[string]uint32{"cluster_1": 75, "cluster_2": 25}, Action: map[string]uint32{"cluster_1": 75, "cluster_2": 25},

View File

@ -319,11 +319,11 @@ func TestXDSResolverGoodServiceUpdate(t *testing.T) {
defer replaceRandNumGenerator(0)() defer replaceRandNumGenerator(0)()
for _, tt := range []struct { for _, tt := range []struct {
su client.ServiceUpdate su xdsclient.ServiceUpdate
wantJSON string wantJSON string
}{ }{
{ {
su: client.ServiceUpdate{Routes: []*client.Route{{Prefix: newStringP(""), Action: map[string]uint32{testCluster1: 1}}}}, su: xdsclient.ServiceUpdate{Routes: []*client.Route{{Prefix: newStringP(""), Action: map[string]uint32{testCluster1: 1}}}},
wantJSON: testOneClusterOnlyJSON, wantJSON: testOneClusterOnlyJSON,
}, },
{ {

View File

@ -18,13 +18,25 @@
package testutils package testutils
import ( import (
"fmt"
"net"
"strconv"
v2xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
v2corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" v2corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
v2endpointpb "github.com/envoyproxy/go-control-plane/envoy/api/v2/endpoint"
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
v2typepb "github.com/envoyproxy/go-control-plane/envoy/type"
wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
"google.golang.org/grpc/xds/internal" "google.golang.org/grpc/xds/internal"
) )
// EmptyNodeProtoV2 is a node proto with no fields set. // EmptyNodeProtoV2 is a v2 Node proto with no fields set.
var EmptyNodeProtoV2 = &v2corepb.Node{} var EmptyNodeProtoV2 = &v2corepb.Node{}
// EmptyNodeProtoV3 is a v3 Node proto with no fields set.
var EmptyNodeProtoV3 = &v3corepb.Node{}
// LocalityIDToProto converts a LocalityID to its proto representation. // LocalityIDToProto converts a LocalityID to its proto representation.
func LocalityIDToProto(l internal.LocalityID) *v2corepb.Locality { func LocalityIDToProto(l internal.LocalityID) *v2corepb.Locality {
return &v2corepb.Locality{ return &v2corepb.Locality{
@ -33,3 +45,101 @@ func LocalityIDToProto(l internal.LocalityID) *v2corepb.Locality {
SubZone: l.SubZone, SubZone: l.SubZone,
} }
} }
// The helper structs/functions related to EDS protos are used in EDS balancer
// tests now, to generate test inputs. Eventually, EDS balancer tests should
// generate EndpointsUpdate directly, instead of generating and parsing the
// proto message.
// TODO: Once EDS balancer tests don't use these, these can be moved to v2 client code.
// ClusterLoadAssignmentBuilder builds a ClusterLoadAssignment, aka EDS
// response.
type ClusterLoadAssignmentBuilder struct {
v *v2xdspb.ClusterLoadAssignment
}
// NewClusterLoadAssignmentBuilder creates a ClusterLoadAssignmentBuilder.
func NewClusterLoadAssignmentBuilder(clusterName string, dropPercents []uint32) *ClusterLoadAssignmentBuilder {
var drops []*v2xdspb.ClusterLoadAssignment_Policy_DropOverload
for i, d := range dropPercents {
drops = append(drops, &v2xdspb.ClusterLoadAssignment_Policy_DropOverload{
Category: fmt.Sprintf("test-drop-%d", i),
DropPercentage: &v2typepb.FractionalPercent{
Numerator: d,
Denominator: v2typepb.FractionalPercent_HUNDRED,
},
})
}
return &ClusterLoadAssignmentBuilder{
v: &v2xdspb.ClusterLoadAssignment{
ClusterName: clusterName,
Policy: &v2xdspb.ClusterLoadAssignment_Policy{
DropOverloads: drops,
},
},
}
}
// AddLocalityOptions contains options when adding locality to the builder.
type AddLocalityOptions struct {
Health []v2corepb.HealthStatus
Weight []uint32
}
// AddLocality adds a locality to the builder.
func (clab *ClusterLoadAssignmentBuilder) AddLocality(subzone string, weight uint32, priority uint32, addrsWithPort []string, opts *AddLocalityOptions) {
var lbEndPoints []*v2endpointpb.LbEndpoint
for i, a := range addrsWithPort {
host, portStr, err := net.SplitHostPort(a)
if err != nil {
panic("failed to split " + a)
}
port, err := strconv.Atoi(portStr)
if err != nil {
panic("failed to atoi " + portStr)
}
lbe := &v2endpointpb.LbEndpoint{
HostIdentifier: &v2endpointpb.LbEndpoint_Endpoint{
Endpoint: &v2endpointpb.Endpoint{
Address: &v2corepb.Address{
Address: &v2corepb.Address_SocketAddress{
SocketAddress: &v2corepb.SocketAddress{
Protocol: v2corepb.SocketAddress_TCP,
Address: host,
PortSpecifier: &v2corepb.SocketAddress_PortValue{
PortValue: uint32(port)}}}}}},
}
if opts != nil {
if i < len(opts.Health) {
lbe.HealthStatus = opts.Health[i]
}
if i < len(opts.Weight) {
lbe.LoadBalancingWeight = &wrapperspb.UInt32Value{Value: opts.Weight[i]}
}
}
lbEndPoints = append(lbEndPoints, lbe)
}
var localityID *v2corepb.Locality
if subzone != "" {
localityID = &v2corepb.Locality{
Region: "",
Zone: "",
SubZone: subzone,
}
}
clab.v.Endpoints = append(clab.v.Endpoints, &v2endpointpb.LocalityLbEndpoints{
Locality: localityID,
LbEndpoints: lbEndPoints,
LoadBalancingWeight: &wrapperspb.UInt32Value{Value: weight},
Priority: priority,
})
}
// Build builds ClusterLoadAssignment.
func (clab *ClusterLoadAssignmentBuilder) Build() *v2xdspb.ClusterLoadAssignment {
return clab.v
}

View File

@ -16,7 +16,8 @@
* *
*/ */
// Package version defines supported xDS API versions. // Package version defines constants to distinguish between supported xDS API
// versions.
package version package version
// TransportAPI refers to the API version for xDS transport protocol. This // TransportAPI refers to the API version for xDS transport protocol. This
@ -30,3 +31,19 @@ const (
// TransportV3 refers to the v3 xDS transport protocol. // TransportV3 refers to the v3 xDS transport protocol.
TransportV3 TransportV3
) )
// Resource URLs. We need to be able to accept either version of the resource
// regardless of the version of the transport protocol in use.
const (
V2ListenerURL = "type.googleapis.com/envoy.api.v2.Listener"
V2RouteConfigURL = "type.googleapis.com/envoy.api.v2.RouteConfiguration"
V2ClusterURL = "type.googleapis.com/envoy.api.v2.Cluster"
V2EndpointsURL = "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment"
V2HTTPConnManagerURL = "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"
V3ListenerURL = "type.googleapis.com/envoy.config.listener.v3.Listener"
V3RouteConfigURL = "type.googleapis.com/envoy.config.route.v3.RouteConfiguration"
V3ClusterURL = "type.googleapis.com/envoy.config.cluster.v3.Cluster"
V3EndpointsURL = "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment"
V3HTTPConnManagerURL = "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"
)

View File

@ -24,6 +24,7 @@
package xds package xds
import ( import (
_ "google.golang.org/grpc/xds/internal/balancer" // Register the balancers. _ "google.golang.org/grpc/xds/internal/balancer" // Register the balancers.
_ "google.golang.org/grpc/xds/internal/resolver" // Register the xds_resolver _ "google.golang.org/grpc/xds/internal/client/v2" // Register the v2 xDS API client.
_ "google.golang.org/grpc/xds/internal/resolver" // Register the xds_resolver.
) )