1/*
2 *
3 * Copyright 2019 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 client
20
21import (
22	"testing"
23	"time"
24
25	xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
26	corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
27	"github.com/golang/protobuf/proto"
28	anypb "github.com/golang/protobuf/ptypes/any"
29	"github.com/google/go-cmp/cmp"
30)
31
32const (
33	serviceName1 = "foo-service"
34	serviceName2 = "bar-service"
35)
36
37func (s) TestValidateCluster(t *testing.T) {
38	emptyUpdate := ClusterUpdate{ServiceName: "", EnableLRS: false}
39	tests := []struct {
40		name       string
41		cluster    *xdspb.Cluster
42		wantUpdate ClusterUpdate
43		wantErr    bool
44	}{
45		{
46			name: "non-eds-cluster-type",
47			cluster: &xdspb.Cluster{
48				ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_STATIC},
49				EdsClusterConfig: &xdspb.Cluster_EdsClusterConfig{
50					EdsConfig: &corepb.ConfigSource{
51						ConfigSourceSpecifier: &corepb.ConfigSource_Ads{
52							Ads: &corepb.AggregatedConfigSource{},
53						},
54					},
55				},
56				LbPolicy: xdspb.Cluster_LEAST_REQUEST,
57			},
58			wantUpdate: emptyUpdate,
59			wantErr:    true,
60		},
61		{
62			name: "no-eds-config",
63			cluster: &xdspb.Cluster{
64				ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS},
65				LbPolicy:             xdspb.Cluster_ROUND_ROBIN,
66			},
67			wantUpdate: emptyUpdate,
68			wantErr:    true,
69		},
70		{
71			name: "no-ads-config-source",
72			cluster: &xdspb.Cluster{
73				ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS},
74				EdsClusterConfig:     &xdspb.Cluster_EdsClusterConfig{},
75				LbPolicy:             xdspb.Cluster_ROUND_ROBIN,
76			},
77			wantUpdate: emptyUpdate,
78			wantErr:    true,
79		},
80		{
81			name: "non-round-robin-lb-policy",
82			cluster: &xdspb.Cluster{
83				ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS},
84				EdsClusterConfig: &xdspb.Cluster_EdsClusterConfig{
85					EdsConfig: &corepb.ConfigSource{
86						ConfigSourceSpecifier: &corepb.ConfigSource_Ads{
87							Ads: &corepb.AggregatedConfigSource{},
88						},
89					},
90				},
91				LbPolicy: xdspb.Cluster_LEAST_REQUEST,
92			},
93			wantUpdate: emptyUpdate,
94			wantErr:    true,
95		},
96		{
97			name: "happy-case-no-service-name-no-lrs",
98			cluster: &xdspb.Cluster{
99				ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS},
100				EdsClusterConfig: &xdspb.Cluster_EdsClusterConfig{
101					EdsConfig: &corepb.ConfigSource{
102						ConfigSourceSpecifier: &corepb.ConfigSource_Ads{
103							Ads: &corepb.AggregatedConfigSource{},
104						},
105					},
106				},
107				LbPolicy: xdspb.Cluster_ROUND_ROBIN,
108			},
109			wantUpdate: emptyUpdate,
110		},
111		{
112			name: "happy-case-no-lrs",
113			cluster: &xdspb.Cluster{
114				ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS},
115				EdsClusterConfig: &xdspb.Cluster_EdsClusterConfig{
116					EdsConfig: &corepb.ConfigSource{
117						ConfigSourceSpecifier: &corepb.ConfigSource_Ads{
118							Ads: &corepb.AggregatedConfigSource{},
119						},
120					},
121					ServiceName: serviceName1,
122				},
123				LbPolicy: xdspb.Cluster_ROUND_ROBIN,
124			},
125			wantUpdate: ClusterUpdate{ServiceName: serviceName1, EnableLRS: false},
126		},
127		{
128			name:       "happiest-case",
129			cluster:    goodCluster1,
130			wantUpdate: ClusterUpdate{ServiceName: serviceName1, EnableLRS: true},
131		},
132	}
133
134	for _, test := range tests {
135		t.Run(test.name, func(t *testing.T) {
136			gotUpdate, gotErr := validateCluster(test.cluster)
137			if (gotErr != nil) != test.wantErr {
138				t.Errorf("validateCluster(%+v) returned error: %v, wantErr: %v", test.cluster, gotErr, test.wantErr)
139			}
140			if !cmp.Equal(gotUpdate, test.wantUpdate) {
141				t.Errorf("validateCluster(%+v) = %v, want: %v", test.cluster, gotUpdate, test.wantUpdate)
142			}
143		})
144	}
145}
146
147// TestCDSHandleResponse starts a fake xDS server, makes a ClientConn to it,
148// and creates a v2Client using it. Then, it registers a CDS watcher and tests
149// different CDS responses.
150func (s) TestCDSHandleResponse(t *testing.T) {
151	tests := []struct {
152		name          string
153		cdsResponse   *xdspb.DiscoveryResponse
154		wantErr       bool
155		wantUpdate    *ClusterUpdate
156		wantUpdateErr bool
157	}{
158		// Badly marshaled CDS response.
159		{
160			name:          "badly-marshaled-response",
161			cdsResponse:   badlyMarshaledCDSResponse,
162			wantErr:       true,
163			wantUpdate:    nil,
164			wantUpdateErr: false,
165		},
166		// Response does not contain Cluster proto.
167		{
168			name:          "no-cluster-proto-in-response",
169			cdsResponse:   badResourceTypeInLDSResponse,
170			wantErr:       true,
171			wantUpdate:    nil,
172			wantUpdateErr: false,
173		},
174		// Response contains no clusters.
175		{
176			name:          "no-cluster",
177			cdsResponse:   &xdspb.DiscoveryResponse{},
178			wantErr:       false,
179			wantUpdate:    nil,
180			wantUpdateErr: false,
181		},
182		// Response contains one good cluster we are not interested in.
183		{
184			name:          "one-uninteresting-cluster",
185			cdsResponse:   goodCDSResponse2,
186			wantErr:       false,
187			wantUpdate:    nil,
188			wantUpdateErr: false,
189		},
190		// Response contains one cluster and it is good.
191		{
192			name:          "one-good-cluster",
193			cdsResponse:   goodCDSResponse1,
194			wantErr:       false,
195			wantUpdate:    &ClusterUpdate{ServiceName: serviceName1, EnableLRS: true},
196			wantUpdateErr: false,
197		},
198	}
199	for _, test := range tests {
200		t.Run(test.name, func(t *testing.T) {
201			testWatchHandle(t, &watchHandleTestcase{
202				typeURL:      cdsURL,
203				resourceName: goodClusterName1,
204
205				responseToHandle: test.cdsResponse,
206				wantHandleErr:    test.wantErr,
207				wantUpdate:       test.wantUpdate,
208				wantUpdateErr:    test.wantUpdateErr,
209			})
210		})
211	}
212}
213
214// TestCDSHandleResponseWithoutWatch tests the case where the v2Client receives
215// a CDS response without a registered watcher.
216func (s) TestCDSHandleResponseWithoutWatch(t *testing.T) {
217	_, cc, cleanup := startServerAndGetCC(t)
218	defer cleanup()
219
220	v2c := newV2Client(&testUpdateReceiver{
221		f: func(string, map[string]interface{}) {},
222	}, cc, goodNodeProto, func(int) time.Duration { return 0 }, nil)
223	defer v2c.close()
224
225	if v2c.handleCDSResponse(badResourceTypeInLDSResponse) == nil {
226		t.Fatal("v2c.handleCDSResponse() succeeded, should have failed")
227	}
228
229	if v2c.handleCDSResponse(goodCDSResponse1) != nil {
230		t.Fatal("v2c.handleCDSResponse() succeeded, should have failed")
231	}
232}
233
234var (
235	badlyMarshaledCDSResponse = &xdspb.DiscoveryResponse{
236		Resources: []*anypb.Any{
237			{
238				TypeUrl: cdsURL,
239				Value:   []byte{1, 2, 3, 4},
240			},
241		},
242		TypeUrl: cdsURL,
243	}
244	goodCluster1 = &xdspb.Cluster{
245		Name:                 goodClusterName1,
246		ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS},
247		EdsClusterConfig: &xdspb.Cluster_EdsClusterConfig{
248			EdsConfig: &corepb.ConfigSource{
249				ConfigSourceSpecifier: &corepb.ConfigSource_Ads{
250					Ads: &corepb.AggregatedConfigSource{},
251				},
252			},
253			ServiceName: serviceName1,
254		},
255		LbPolicy: xdspb.Cluster_ROUND_ROBIN,
256		LrsServer: &corepb.ConfigSource{
257			ConfigSourceSpecifier: &corepb.ConfigSource_Self{
258				Self: &corepb.SelfConfigSource{},
259			},
260		},
261	}
262	marshaledCluster1, _ = proto.Marshal(goodCluster1)
263	goodCluster2         = &xdspb.Cluster{
264		Name:                 goodClusterName2,
265		ClusterDiscoveryType: &xdspb.Cluster_Type{Type: xdspb.Cluster_EDS},
266		EdsClusterConfig: &xdspb.Cluster_EdsClusterConfig{
267			EdsConfig: &corepb.ConfigSource{
268				ConfigSourceSpecifier: &corepb.ConfigSource_Ads{
269					Ads: &corepb.AggregatedConfigSource{},
270				},
271			},
272			ServiceName: serviceName2,
273		},
274		LbPolicy: xdspb.Cluster_ROUND_ROBIN,
275	}
276	marshaledCluster2, _ = proto.Marshal(goodCluster2)
277	goodCDSResponse1     = &xdspb.DiscoveryResponse{
278		Resources: []*anypb.Any{
279			{
280				TypeUrl: cdsURL,
281				Value:   marshaledCluster1,
282			},
283		},
284		TypeUrl: cdsURL,
285	}
286	goodCDSResponse2 = &xdspb.DiscoveryResponse{
287		Resources: []*anypb.Any{
288			{
289				TypeUrl: cdsURL,
290				Value:   marshaledCluster2,
291			},
292		},
293		TypeUrl: cdsURL,
294	}
295)
296