1/*
2 *
3 * Copyright 2021 gRPC authors.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18
19package xdsclient_test
20
21import (
22	"fmt"
23	"testing"
24	"time"
25
26	v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
27	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
28	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
29	v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
30	v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
31	"github.com/google/go-cmp/cmp"
32	"github.com/google/go-cmp/cmp/cmpopts"
33	"google.golang.org/protobuf/testing/protocmp"
34	"google.golang.org/protobuf/types/known/anypb"
35	"google.golang.org/protobuf/types/known/durationpb"
36
37	"google.golang.org/grpc"
38	"google.golang.org/grpc/credentials/insecure"
39	"google.golang.org/grpc/internal/testutils"
40	xdstestutils "google.golang.org/grpc/xds/internal/testutils"
41	"google.golang.org/grpc/xds/internal/xdsclient"
42	"google.golang.org/grpc/xds/internal/xdsclient/bootstrap"
43)
44
45const defaultTestWatchExpiryTimeout = 500 * time.Millisecond
46
47func (s) TestLDSConfigDump(t *testing.T) {
48	const testVersion = "test-version-lds"
49	var (
50		ldsTargets       = []string{"lds.target.good:0000", "lds.target.good:1111"}
51		routeConfigNames = []string{"route-config-0", "route-config-1"}
52		listenerRaws     = make(map[string]*anypb.Any, len(ldsTargets))
53	)
54
55	for i := range ldsTargets {
56		listenersT := &v3listenerpb.Listener{
57			Name: ldsTargets[i],
58			ApiListener: &v3listenerpb.ApiListener{
59				ApiListener: testutils.MarshalAny(&v3httppb.HttpConnectionManager{
60					RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
61						Rds: &v3httppb.Rds{
62							ConfigSource: &v3corepb.ConfigSource{
63								ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
64							},
65							RouteConfigName: routeConfigNames[i],
66						},
67					},
68					CommonHttpProtocolOptions: &v3corepb.HttpProtocolOptions{
69						MaxStreamDuration: durationpb.New(time.Second),
70					},
71				}),
72			},
73		}
74		listenerRaws[ldsTargets[i]] = testutils.MarshalAny(listenersT)
75	}
76
77	client, err := xdsclient.NewWithConfigForTesting(&bootstrap.Config{
78		BalancerName: testXDSServer,
79		Creds:        grpc.WithTransportCredentials(insecure.NewCredentials()),
80		NodeProto:    xdstestutils.EmptyNodeProtoV2,
81	}, defaultTestWatchExpiryTimeout)
82	if err != nil {
83		t.Fatalf("failed to create client: %v", err)
84	}
85	defer client.Close()
86	updateHandler := client.(xdsclient.UpdateHandler)
87
88	// Expected unknown.
89	if err := compareDump(client.DumpLDS, "", map[string]xdsclient.UpdateWithMD{}); err != nil {
90		t.Fatalf(err.Error())
91	}
92
93	wantRequested := make(map[string]xdsclient.UpdateWithMD)
94	for _, n := range ldsTargets {
95		cancel := client.WatchListener(n, func(update xdsclient.ListenerUpdate, err error) {})
96		defer cancel()
97		wantRequested[n] = xdsclient.UpdateWithMD{MD: xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusRequested}}
98	}
99	// Expected requested.
100	if err := compareDump(client.DumpLDS, "", wantRequested); err != nil {
101		t.Fatalf(err.Error())
102	}
103
104	update0 := make(map[string]xdsclient.ListenerUpdateErrTuple)
105	want0 := make(map[string]xdsclient.UpdateWithMD)
106	for n, r := range listenerRaws {
107		update0[n] = xdsclient.ListenerUpdateErrTuple{Update: xdsclient.ListenerUpdate{Raw: r}}
108		want0[n] = xdsclient.UpdateWithMD{
109			MD:  xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusACKed, Version: testVersion},
110			Raw: r,
111		}
112	}
113	updateHandler.NewListeners(update0, xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusACKed, Version: testVersion})
114
115	// Expect ACK.
116	if err := compareDump(client.DumpLDS, testVersion, want0); err != nil {
117		t.Fatalf(err.Error())
118	}
119
120	const nackVersion = "lds-version-nack"
121	var nackErr = fmt.Errorf("lds nack error")
122	updateHandler.NewListeners(
123		map[string]xdsclient.ListenerUpdateErrTuple{
124			ldsTargets[0]: {Err: nackErr},
125			ldsTargets[1]: {Update: xdsclient.ListenerUpdate{Raw: listenerRaws[ldsTargets[1]]}},
126		},
127		xdsclient.UpdateMetadata{
128			Status: xdsclient.ServiceStatusNACKed,
129			ErrState: &xdsclient.UpdateErrorMetadata{
130				Version: nackVersion,
131				Err:     nackErr,
132			},
133		},
134	)
135
136	// Expect NACK for [0], but old ACK for [1].
137	wantDump := make(map[string]xdsclient.UpdateWithMD)
138	// Though resource 0 was NACKed, the dump should show the previous ACKed raw
139	// message, as well as the NACK error.
140	wantDump[ldsTargets[0]] = xdsclient.UpdateWithMD{
141		MD: xdsclient.UpdateMetadata{
142			Status:  xdsclient.ServiceStatusNACKed,
143			Version: testVersion,
144			ErrState: &xdsclient.UpdateErrorMetadata{
145				Version: nackVersion,
146				Err:     nackErr,
147			},
148		},
149		Raw: listenerRaws[ldsTargets[0]],
150	}
151
152	wantDump[ldsTargets[1]] = xdsclient.UpdateWithMD{
153		MD:  xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusACKed, Version: nackVersion},
154		Raw: listenerRaws[ldsTargets[1]],
155	}
156	if err := compareDump(client.DumpLDS, nackVersion, wantDump); err != nil {
157		t.Fatalf(err.Error())
158	}
159}
160
161func (s) TestRDSConfigDump(t *testing.T) {
162	const testVersion = "test-version-rds"
163	var (
164		listenerNames = []string{"lds.target.good:0000", "lds.target.good:1111"}
165		rdsTargets    = []string{"route-config-0", "route-config-1"}
166		clusterNames  = []string{"cluster-0", "cluster-1"}
167		routeRaws     = make(map[string]*anypb.Any, len(rdsTargets))
168	)
169
170	for i := range rdsTargets {
171		routeConfigT := &v3routepb.RouteConfiguration{
172			Name: rdsTargets[i],
173			VirtualHosts: []*v3routepb.VirtualHost{
174				{
175					Domains: []string{listenerNames[i]},
176					Routes: []*v3routepb.Route{{
177						Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}},
178						Action: &v3routepb.Route_Route{
179							Route: &v3routepb.RouteAction{
180								ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterNames[i]},
181							},
182						},
183					}},
184				},
185			},
186		}
187
188		routeRaws[rdsTargets[i]] = testutils.MarshalAny(routeConfigT)
189	}
190
191	client, err := xdsclient.NewWithConfigForTesting(&bootstrap.Config{
192		BalancerName: testXDSServer,
193		Creds:        grpc.WithTransportCredentials(insecure.NewCredentials()),
194		NodeProto:    xdstestutils.EmptyNodeProtoV2,
195	}, defaultTestWatchExpiryTimeout)
196	if err != nil {
197		t.Fatalf("failed to create client: %v", err)
198	}
199	defer client.Close()
200	updateHandler := client.(xdsclient.UpdateHandler)
201
202	// Expected unknown.
203	if err := compareDump(client.DumpRDS, "", map[string]xdsclient.UpdateWithMD{}); err != nil {
204		t.Fatalf(err.Error())
205	}
206
207	wantRequested := make(map[string]xdsclient.UpdateWithMD)
208	for _, n := range rdsTargets {
209		cancel := client.WatchRouteConfig(n, func(update xdsclient.RouteConfigUpdate, err error) {})
210		defer cancel()
211		wantRequested[n] = xdsclient.UpdateWithMD{MD: xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusRequested}}
212	}
213	// Expected requested.
214	if err := compareDump(client.DumpRDS, "", wantRequested); err != nil {
215		t.Fatalf(err.Error())
216	}
217
218	update0 := make(map[string]xdsclient.RouteConfigUpdateErrTuple)
219	want0 := make(map[string]xdsclient.UpdateWithMD)
220	for n, r := range routeRaws {
221		update0[n] = xdsclient.RouteConfigUpdateErrTuple{Update: xdsclient.RouteConfigUpdate{Raw: r}}
222		want0[n] = xdsclient.UpdateWithMD{
223			MD:  xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusACKed, Version: testVersion},
224			Raw: r,
225		}
226	}
227	updateHandler.NewRouteConfigs(update0, xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusACKed, Version: testVersion})
228
229	// Expect ACK.
230	if err := compareDump(client.DumpRDS, testVersion, want0); err != nil {
231		t.Fatalf(err.Error())
232	}
233
234	const nackVersion = "rds-version-nack"
235	var nackErr = fmt.Errorf("rds nack error")
236	updateHandler.NewRouteConfigs(
237		map[string]xdsclient.RouteConfigUpdateErrTuple{
238			rdsTargets[0]: {Err: nackErr},
239			rdsTargets[1]: {Update: xdsclient.RouteConfigUpdate{Raw: routeRaws[rdsTargets[1]]}},
240		},
241		xdsclient.UpdateMetadata{
242			Status: xdsclient.ServiceStatusNACKed,
243			ErrState: &xdsclient.UpdateErrorMetadata{
244				Version: nackVersion,
245				Err:     nackErr,
246			},
247		},
248	)
249
250	// Expect NACK for [0], but old ACK for [1].
251	wantDump := make(map[string]xdsclient.UpdateWithMD)
252	// Though resource 0 was NACKed, the dump should show the previous ACKed raw
253	// message, as well as the NACK error.
254	wantDump[rdsTargets[0]] = xdsclient.UpdateWithMD{
255		MD: xdsclient.UpdateMetadata{
256			Status:  xdsclient.ServiceStatusNACKed,
257			Version: testVersion,
258			ErrState: &xdsclient.UpdateErrorMetadata{
259				Version: nackVersion,
260				Err:     nackErr,
261			},
262		},
263		Raw: routeRaws[rdsTargets[0]],
264	}
265	wantDump[rdsTargets[1]] = xdsclient.UpdateWithMD{
266		MD:  xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusACKed, Version: nackVersion},
267		Raw: routeRaws[rdsTargets[1]],
268	}
269	if err := compareDump(client.DumpRDS, nackVersion, wantDump); err != nil {
270		t.Fatalf(err.Error())
271	}
272}
273
274func (s) TestCDSConfigDump(t *testing.T) {
275	const testVersion = "test-version-cds"
276	var (
277		cdsTargets   = []string{"cluster-0", "cluster-1"}
278		serviceNames = []string{"service-0", "service-1"}
279		clusterRaws  = make(map[string]*anypb.Any, len(cdsTargets))
280	)
281
282	for i := range cdsTargets {
283		clusterT := &v3clusterpb.Cluster{
284			Name:                 cdsTargets[i],
285			ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS},
286			EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{
287				EdsConfig: &v3corepb.ConfigSource{
288					ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{
289						Ads: &v3corepb.AggregatedConfigSource{},
290					},
291				},
292				ServiceName: serviceNames[i],
293			},
294			LbPolicy: v3clusterpb.Cluster_ROUND_ROBIN,
295			LrsServer: &v3corepb.ConfigSource{
296				ConfigSourceSpecifier: &v3corepb.ConfigSource_Self{
297					Self: &v3corepb.SelfConfigSource{},
298				},
299			},
300		}
301
302		clusterRaws[cdsTargets[i]] = testutils.MarshalAny(clusterT)
303	}
304
305	client, err := xdsclient.NewWithConfigForTesting(&bootstrap.Config{
306		BalancerName: testXDSServer,
307		Creds:        grpc.WithTransportCredentials(insecure.NewCredentials()),
308		NodeProto:    xdstestutils.EmptyNodeProtoV2,
309	}, defaultTestWatchExpiryTimeout)
310	if err != nil {
311		t.Fatalf("failed to create client: %v", err)
312	}
313	defer client.Close()
314	updateHandler := client.(xdsclient.UpdateHandler)
315
316	// Expected unknown.
317	if err := compareDump(client.DumpCDS, "", map[string]xdsclient.UpdateWithMD{}); err != nil {
318		t.Fatalf(err.Error())
319	}
320
321	wantRequested := make(map[string]xdsclient.UpdateWithMD)
322	for _, n := range cdsTargets {
323		cancel := client.WatchCluster(n, func(update xdsclient.ClusterUpdate, err error) {})
324		defer cancel()
325		wantRequested[n] = xdsclient.UpdateWithMD{MD: xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusRequested}}
326	}
327	// Expected requested.
328	if err := compareDump(client.DumpCDS, "", wantRequested); err != nil {
329		t.Fatalf(err.Error())
330	}
331
332	update0 := make(map[string]xdsclient.ClusterUpdateErrTuple)
333	want0 := make(map[string]xdsclient.UpdateWithMD)
334	for n, r := range clusterRaws {
335		update0[n] = xdsclient.ClusterUpdateErrTuple{Update: xdsclient.ClusterUpdate{Raw: r}}
336		want0[n] = xdsclient.UpdateWithMD{
337			MD:  xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusACKed, Version: testVersion},
338			Raw: r,
339		}
340	}
341	updateHandler.NewClusters(update0, xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusACKed, Version: testVersion})
342
343	// Expect ACK.
344	if err := compareDump(client.DumpCDS, testVersion, want0); err != nil {
345		t.Fatalf(err.Error())
346	}
347
348	const nackVersion = "cds-version-nack"
349	var nackErr = fmt.Errorf("cds nack error")
350	updateHandler.NewClusters(
351		map[string]xdsclient.ClusterUpdateErrTuple{
352			cdsTargets[0]: {Err: nackErr},
353			cdsTargets[1]: {Update: xdsclient.ClusterUpdate{Raw: clusterRaws[cdsTargets[1]]}},
354		},
355		xdsclient.UpdateMetadata{
356			Status: xdsclient.ServiceStatusNACKed,
357			ErrState: &xdsclient.UpdateErrorMetadata{
358				Version: nackVersion,
359				Err:     nackErr,
360			},
361		},
362	)
363
364	// Expect NACK for [0], but old ACK for [1].
365	wantDump := make(map[string]xdsclient.UpdateWithMD)
366	// Though resource 0 was NACKed, the dump should show the previous ACKed raw
367	// message, as well as the NACK error.
368	wantDump[cdsTargets[0]] = xdsclient.UpdateWithMD{
369		MD: xdsclient.UpdateMetadata{
370			Status:  xdsclient.ServiceStatusNACKed,
371			Version: testVersion,
372			ErrState: &xdsclient.UpdateErrorMetadata{
373				Version: nackVersion,
374				Err:     nackErr,
375			},
376		},
377		Raw: clusterRaws[cdsTargets[0]],
378	}
379	wantDump[cdsTargets[1]] = xdsclient.UpdateWithMD{
380		MD:  xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusACKed, Version: nackVersion},
381		Raw: clusterRaws[cdsTargets[1]],
382	}
383	if err := compareDump(client.DumpCDS, nackVersion, wantDump); err != nil {
384		t.Fatalf(err.Error())
385	}
386}
387
388func (s) TestEDSConfigDump(t *testing.T) {
389	const testVersion = "test-version-cds"
390	var (
391		edsTargets    = []string{"cluster-0", "cluster-1"}
392		localityNames = []string{"locality-0", "locality-1"}
393		addrs         = []string{"addr0:123", "addr1:456"}
394		endpointRaws  = make(map[string]*anypb.Any, len(edsTargets))
395	)
396
397	for i := range edsTargets {
398		clab0 := xdstestutils.NewClusterLoadAssignmentBuilder(edsTargets[i], nil)
399		clab0.AddLocality(localityNames[i], 1, 1, []string{addrs[i]}, nil)
400		claT := clab0.Build()
401
402		endpointRaws[edsTargets[i]] = testutils.MarshalAny(claT)
403	}
404
405	client, err := xdsclient.NewWithConfigForTesting(&bootstrap.Config{
406		BalancerName: testXDSServer,
407		Creds:        grpc.WithTransportCredentials(insecure.NewCredentials()),
408		NodeProto:    xdstestutils.EmptyNodeProtoV2,
409	}, defaultTestWatchExpiryTimeout)
410	if err != nil {
411		t.Fatalf("failed to create client: %v", err)
412	}
413	defer client.Close()
414	updateHandler := client.(xdsclient.UpdateHandler)
415
416	// Expected unknown.
417	if err := compareDump(client.DumpEDS, "", map[string]xdsclient.UpdateWithMD{}); err != nil {
418		t.Fatalf(err.Error())
419	}
420
421	wantRequested := make(map[string]xdsclient.UpdateWithMD)
422	for _, n := range edsTargets {
423		cancel := client.WatchEndpoints(n, func(update xdsclient.EndpointsUpdate, err error) {})
424		defer cancel()
425		wantRequested[n] = xdsclient.UpdateWithMD{MD: xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusRequested}}
426	}
427	// Expected requested.
428	if err := compareDump(client.DumpEDS, "", wantRequested); err != nil {
429		t.Fatalf(err.Error())
430	}
431
432	update0 := make(map[string]xdsclient.EndpointsUpdateErrTuple)
433	want0 := make(map[string]xdsclient.UpdateWithMD)
434	for n, r := range endpointRaws {
435		update0[n] = xdsclient.EndpointsUpdateErrTuple{Update: xdsclient.EndpointsUpdate{Raw: r}}
436		want0[n] = xdsclient.UpdateWithMD{
437			MD:  xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusACKed, Version: testVersion},
438			Raw: r,
439		}
440	}
441	updateHandler.NewEndpoints(update0, xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusACKed, Version: testVersion})
442
443	// Expect ACK.
444	if err := compareDump(client.DumpEDS, testVersion, want0); err != nil {
445		t.Fatalf(err.Error())
446	}
447
448	const nackVersion = "eds-version-nack"
449	var nackErr = fmt.Errorf("eds nack error")
450	updateHandler.NewEndpoints(
451		map[string]xdsclient.EndpointsUpdateErrTuple{
452			edsTargets[0]: {Err: nackErr},
453			edsTargets[1]: {Update: xdsclient.EndpointsUpdate{Raw: endpointRaws[edsTargets[1]]}},
454		},
455		xdsclient.UpdateMetadata{
456			Status: xdsclient.ServiceStatusNACKed,
457			ErrState: &xdsclient.UpdateErrorMetadata{
458				Version: nackVersion,
459				Err:     nackErr,
460			},
461		},
462	)
463
464	// Expect NACK for [0], but old ACK for [1].
465	wantDump := make(map[string]xdsclient.UpdateWithMD)
466	// Though resource 0 was NACKed, the dump should show the previous ACKed raw
467	// message, as well as the NACK error.
468	wantDump[edsTargets[0]] = xdsclient.UpdateWithMD{
469		MD: xdsclient.UpdateMetadata{
470			Status:  xdsclient.ServiceStatusNACKed,
471			Version: testVersion,
472			ErrState: &xdsclient.UpdateErrorMetadata{
473				Version: nackVersion,
474				Err:     nackErr,
475			},
476		},
477		Raw: endpointRaws[edsTargets[0]],
478	}
479	wantDump[edsTargets[1]] = xdsclient.UpdateWithMD{
480		MD:  xdsclient.UpdateMetadata{Status: xdsclient.ServiceStatusACKed, Version: nackVersion},
481		Raw: endpointRaws[edsTargets[1]],
482	}
483	if err := compareDump(client.DumpEDS, nackVersion, wantDump); err != nil {
484		t.Fatalf(err.Error())
485	}
486}
487
488func compareDump(dumpFunc func() (string, map[string]xdsclient.UpdateWithMD), wantVersion string, wantDump interface{}) error {
489	v, dump := dumpFunc()
490	if v != wantVersion {
491		return fmt.Errorf("Dump() returned version %q, want %q", v, wantVersion)
492	}
493	cmpOpts := cmp.Options{
494		cmpopts.EquateEmpty(),
495		cmp.Comparer(func(a, b time.Time) bool { return true }),
496		cmp.Comparer(func(x, y error) bool {
497			if x == nil || y == nil {
498				return x == nil && y == nil
499			}
500			return x.Error() == y.Error()
501		}),
502		protocmp.Transform(),
503	}
504	if diff := cmp.Diff(dump, wantDump, cmpOpts); diff != "" {
505		return fmt.Errorf("Dump() returned unexpected dump, diff (-got +want): %s", diff)
506	}
507	return nil
508}
509