1 package destination
2
3 import (
4 "testing"
5
6 "github.com/golang/protobuf/ptypes/duration"
7 pb "github.com/linkerd/linkerd2-proxy-api/go/destination"
8 httpPb "github.com/linkerd/linkerd2-proxy-api/go/http_types"
9 sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
10 logging "github.com/sirupsen/logrus"
11 "google.golang.org/protobuf/proto"
12 )
13
14 var (
15 getButNotPrivate = &sp.RequestMatch{
16 All: []*sp.RequestMatch{
17 {
18 Method: "GET",
19 },
20 {
21 Not: &sp.RequestMatch{
22 PathRegex: "/private/.*",
23 },
24 },
25 },
26 }
27
28 pbGetButNotPrivate = &pb.RequestMatch{
29 Match: &pb.RequestMatch_All{
30 All: &pb.RequestMatch_Seq{
31 Matches: []*pb.RequestMatch{
32 {
33 Match: &pb.RequestMatch_Method{
34 Method: &httpPb.HttpMethod{
35 Type: &httpPb.HttpMethod_Registered_{
36 Registered: httpPb.HttpMethod_GET,
37 },
38 },
39 },
40 },
41 {
42 Match: &pb.RequestMatch_Not{
43 Not: &pb.RequestMatch{
44 Match: &pb.RequestMatch_Path{
45 Path: &pb.PathMatch{
46 Regex: "/private/.*",
47 },
48 },
49 },
50 },
51 },
52 },
53 },
54 },
55 }
56
57 login = &sp.RequestMatch{
58 PathRegex: "/login",
59 }
60
61 pbLogin = &pb.RequestMatch{
62 Match: &pb.RequestMatch_Path{
63 Path: &pb.PathMatch{
64 Regex: "/login",
65 },
66 },
67 }
68
69 fiveXX = &sp.ResponseMatch{
70 Status: &sp.Range{
71 Min: 500,
72 Max: 599,
73 },
74 }
75
76 pbFiveXX = &pb.ResponseMatch{
77 Match: &pb.ResponseMatch_Status{
78 Status: &pb.HttpStatusRange{
79 Min: 500,
80 Max: 599,
81 },
82 },
83 }
84
85 fiveXXfourTwenty = &sp.ResponseMatch{
86 Any: []*sp.ResponseMatch{
87 fiveXX,
88 {
89 Status: &sp.Range{
90 Min: 420,
91 Max: 420,
92 },
93 },
94 },
95 }
96
97 pbFiveXXfourTwenty = &pb.ResponseMatch{
98 Match: &pb.ResponseMatch_Any{
99 Any: &pb.ResponseMatch_Seq{
100 Matches: []*pb.ResponseMatch{
101 pbFiveXX,
102 {
103 Match: &pb.ResponseMatch_Status{
104 Status: &pb.HttpStatusRange{
105 Min: 420,
106 Max: 420,
107 },
108 },
109 },
110 },
111 },
112 },
113 }
114
115 route1 = &sp.RouteSpec{
116 Name: "route1",
117 Condition: getButNotPrivate,
118 ResponseClasses: []*sp.ResponseClass{
119 {
120 Condition: fiveXX,
121 IsFailure: true,
122 },
123 },
124 }
125
126 pbRoute1 = &pb.Route{
127 MetricsLabels: map[string]string{
128 "route": "route1",
129 },
130 Condition: pbGetButNotPrivate,
131 ResponseClasses: []*pb.ResponseClass{
132 {
133 Condition: pbFiveXX,
134 IsFailure: true,
135 },
136 },
137 Timeout: nil,
138 }
139
140 route2 = &sp.RouteSpec{
141 Name: "route2",
142 Condition: login,
143 ResponseClasses: []*sp.ResponseClass{
144 {
145 Condition: fiveXXfourTwenty,
146 IsFailure: true,
147 },
148 },
149 }
150
151 pbRoute2 = &pb.Route{
152 MetricsLabels: map[string]string{
153 "route": "route2",
154 },
155 Condition: pbLogin,
156 ResponseClasses: []*pb.ResponseClass{
157 {
158 Condition: pbFiveXXfourTwenty,
159 IsFailure: true,
160 },
161 },
162 Timeout: nil,
163 }
164
165 profile = &sp.ServiceProfile{
166 Spec: sp.ServiceProfileSpec{
167 Routes: []*sp.RouteSpec{
168 route1,
169 route2,
170 },
171 },
172 }
173
174 pbProfile = &pb.DestinationProfile{
175 Routes: []*pb.Route{
176 pbRoute1,
177 pbRoute2,
178 },
179 RetryBudget: defaultRetryBudget(),
180 }
181
182 defaultPbProfile = &pb.DestinationProfile{
183 Routes: []*pb.Route{},
184 RetryBudget: defaultRetryBudget(),
185 }
186
187 multipleRequestMatches = &sp.ServiceProfile{
188 Spec: sp.ServiceProfileSpec{
189 Routes: []*sp.RouteSpec{
190 {
191 Name: "multipleRequestMatches",
192 Condition: &sp.RequestMatch{
193 Method: "GET",
194 PathRegex: "/my/path",
195 },
196 },
197 },
198 },
199 }
200
201 pbRequestMatchAll = &pb.DestinationProfile{
202 Routes: []*pb.Route{
203 {
204 Condition: &pb.RequestMatch{
205 Match: &pb.RequestMatch_All{
206 All: &pb.RequestMatch_Seq{
207 Matches: []*pb.RequestMatch{
208 {
209 Match: &pb.RequestMatch_Method{
210 Method: &httpPb.HttpMethod{
211 Type: &httpPb.HttpMethod_Registered_{
212 Registered: httpPb.HttpMethod_GET,
213 },
214 },
215 },
216 },
217 {
218 Match: &pb.RequestMatch_Path{
219 Path: &pb.PathMatch{
220 Regex: "/my/path",
221 },
222 },
223 },
224 },
225 },
226 },
227 },
228 MetricsLabels: map[string]string{
229 "route": "multipleRequestMatches",
230 },
231 ResponseClasses: []*pb.ResponseClass{},
232 Timeout: nil,
233 },
234 },
235 RetryBudget: defaultRetryBudget(),
236 }
237
238 notEnoughRequestMatches = &sp.ServiceProfile{
239 Spec: sp.ServiceProfileSpec{
240 Routes: []*sp.RouteSpec{
241 {
242 Condition: &sp.RequestMatch{},
243 },
244 },
245 },
246 }
247
248 multipleResponseMatches = &sp.ServiceProfile{
249 Spec: sp.ServiceProfileSpec{
250 Routes: []*sp.RouteSpec{
251 {
252 Name: "multipleResponseMatches",
253 Condition: &sp.RequestMatch{
254 Method: "GET",
255 },
256 ResponseClasses: []*sp.ResponseClass{
257 {
258 Condition: &sp.ResponseMatch{
259 Status: &sp.Range{
260 Min: 400,
261 Max: 499,
262 },
263 Not: &sp.ResponseMatch{
264 Status: &sp.Range{
265 Min: 404,
266 },
267 },
268 },
269 },
270 },
271 },
272 },
273 },
274 }
275
276 pbResponseMatchAll = &pb.DestinationProfile{
277 Routes: []*pb.Route{
278 {
279 Condition: &pb.RequestMatch{
280 Match: &pb.RequestMatch_Method{
281 Method: &httpPb.HttpMethod{
282 Type: &httpPb.HttpMethod_Registered_{
283 Registered: httpPb.HttpMethod_GET,
284 },
285 },
286 },
287 },
288 MetricsLabels: map[string]string{
289 "route": "multipleResponseMatches",
290 },
291 ResponseClasses: []*pb.ResponseClass{
292 {
293 Condition: &pb.ResponseMatch{
294 Match: &pb.ResponseMatch_All{
295 All: &pb.ResponseMatch_Seq{
296 Matches: []*pb.ResponseMatch{
297 {
298 Match: &pb.ResponseMatch_Status{
299 Status: &pb.HttpStatusRange{
300 Min: 400,
301 Max: 499,
302 },
303 },
304 },
305 {
306 Match: &pb.ResponseMatch_Not{
307 Not: &pb.ResponseMatch{
308 Match: &pb.ResponseMatch_Status{
309 Status: &pb.HttpStatusRange{
310 Min: 404,
311 },
312 },
313 },
314 },
315 },
316 },
317 },
318 },
319 },
320 },
321 },
322 Timeout: nil,
323 },
324 },
325 RetryBudget: defaultRetryBudget(),
326 }
327
328 oneSidedStatusRange = &sp.ServiceProfile{
329 Spec: sp.ServiceProfileSpec{
330 Routes: []*sp.RouteSpec{
331 {
332 Condition: &sp.RequestMatch{
333 Method: "GET",
334 },
335 ResponseClasses: []*sp.ResponseClass{
336 {
337 Condition: &sp.ResponseMatch{
338 Status: &sp.Range{
339 Min: 200,
340 },
341 },
342 },
343 },
344 },
345 },
346 },
347 }
348
349 invalidStatusRange = &sp.ServiceProfile{
350 Spec: sp.ServiceProfileSpec{
351 Routes: []*sp.RouteSpec{
352 {
353 Condition: &sp.RequestMatch{
354 Method: "GET",
355 },
356 ResponseClasses: []*sp.ResponseClass{
357 {
358 Condition: &sp.ResponseMatch{
359 Status: &sp.Range{
360 Min: 201,
361 Max: 200,
362 },
363 },
364 },
365 },
366 },
367 },
368 },
369 }
370
371 notEnoughResponseMatches = &sp.ServiceProfile{
372 Spec: sp.ServiceProfileSpec{
373 Routes: []*sp.RouteSpec{
374 {
375 Condition: &sp.RequestMatch{
376 Method: "GET",
377 },
378 ResponseClasses: []*sp.ResponseClass{
379 {
380 Condition: &sp.ResponseMatch{},
381 },
382 },
383 },
384 },
385 },
386 }
387
388 routeWithTimeout = &sp.RouteSpec{
389 Name: "routeWithTimeout",
390 Condition: login,
391 ResponseClasses: []*sp.ResponseClass{},
392 Timeout: "200ms",
393 }
394
395 profileWithTimeout = &sp.ServiceProfile{
396 Spec: sp.ServiceProfileSpec{
397 Routes: []*sp.RouteSpec{
398 routeWithTimeout,
399 },
400 },
401 }
402
403 pbRouteWithTimeout = &pb.Route{
404 MetricsLabels: map[string]string{
405 "route": "routeWithTimeout",
406 },
407 Condition: pbLogin,
408 ResponseClasses: []*pb.ResponseClass{},
409 Timeout: &duration.Duration{
410 Nanos: 200000000,
411 },
412 }
413
414 pbProfileWithTimeout = &pb.DestinationProfile{
415 Routes: []*pb.Route{
416 pbRouteWithTimeout,
417 },
418 RetryBudget: defaultRetryBudget(),
419 }
420 )
421
422 func TestProfileTranslator(t *testing.T) {
423 t.Run("Sends update", func(t *testing.T) {
424 mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
425
426 translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
427 translator.Start()
428 defer translator.Stop()
429
430 translator.Update(profile)
431
432 actualPbProfile := <-mockGetProfileServer.profilesReceived
433 if !proto.Equal(actualPbProfile, pbProfile) {
434 t.Fatalf("Expected profile sent to be [%v] but was [%v]", pbProfile, actualPbProfile)
435 }
436 numProfiles := len(mockGetProfileServer.profilesReceived) + 1
437 if numProfiles != 1 {
438 t.Fatalf("Expecting [1] profile, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
439 }
440 })
441
442 t.Run("Request match with more than one field becomes ALL", func(t *testing.T) {
443 mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
444
445 translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
446 translator.Start()
447 defer translator.Stop()
448
449 translator.Update(multipleRequestMatches)
450
451 actualPbProfile := <-mockGetProfileServer.profilesReceived
452 if !proto.Equal(actualPbProfile, pbRequestMatchAll) {
453 t.Fatalf("Expected profile sent to be [%v] but was [%v]", pbRequestMatchAll, actualPbProfile)
454 }
455 numProfiles := len(mockGetProfileServer.profilesReceived) + 1
456 if numProfiles != 1 {
457 t.Fatalf("Expecting [1] profiles, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
458 }
459 })
460
461 t.Run("Ignores request match without any fields", func(t *testing.T) {
462 mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
463
464 translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
465 translator.Start()
466 defer translator.Stop()
467
468 translator.Update(notEnoughRequestMatches)
469
470 numProfiles := len(mockGetProfileServer.profilesReceived)
471 if numProfiles != 0 {
472 t.Fatalf("Expecting [0] profiles, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
473 }
474 })
475
476 t.Run("Response match with more than one field becomes ALL", func(t *testing.T) {
477 mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
478
479 translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
480 translator.Start()
481 defer translator.Stop()
482
483 translator.Update(multipleResponseMatches)
484
485 actualPbProfile := <-mockGetProfileServer.profilesReceived
486 if !proto.Equal(actualPbProfile, pbResponseMatchAll) {
487 t.Fatalf("Expected profile sent to be [%v] but was [%v]", pbResponseMatchAll, actualPbProfile)
488 }
489 numProfiles := len(mockGetProfileServer.profilesReceived) + 1
490 if numProfiles != 1 {
491 t.Fatalf("Expecting [1] profiles, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
492 }
493 })
494
495 t.Run("Ignores response match without any fields", func(t *testing.T) {
496 mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
497
498 translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
499 translator.Start()
500 defer translator.Stop()
501
502 translator.Update(notEnoughResponseMatches)
503
504 numProfiles := len(mockGetProfileServer.profilesReceived)
505 if numProfiles != 0 {
506 t.Fatalf("Expecting [0] profiles, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
507 }
508 })
509
510 t.Run("Ignores response match with invalid status range", func(t *testing.T) {
511 mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
512
513 translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
514 translator.Start()
515 defer translator.Stop()
516
517 translator.Update(invalidStatusRange)
518
519 numProfiles := len(mockGetProfileServer.profilesReceived)
520 if numProfiles != 0 {
521 t.Fatalf("Expecting [0] profiles, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
522 }
523 })
524
525 t.Run("Sends update for one sided status range", func(t *testing.T) {
526 mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
527
528 translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
529 translator.Start()
530 defer translator.Stop()
531
532 translator.Update(oneSidedStatusRange)
533
534 <-mockGetProfileServer.profilesReceived
535
536 numProfiles := len(mockGetProfileServer.profilesReceived) + 1
537 if numProfiles != 1 {
538 t.Fatalf("Expecting [1] profile, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
539 }
540 })
541
542 t.Run("Sends empty update", func(t *testing.T) {
543 mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
544
545 translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
546 translator.Start()
547 defer translator.Stop()
548
549 translator.Update(nil)
550
551 actualPbProfile := <-mockGetProfileServer.profilesReceived
552 if !proto.Equal(actualPbProfile, defaultPbProfile) {
553 t.Fatalf("Expected profile sent to be [%v] but was [%v]", defaultPbProfile, actualPbProfile)
554 }
555 numProfiles := len(mockGetProfileServer.profilesReceived) + 1
556 if numProfiles != 1 {
557 t.Fatalf("Expecting [1] profile, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
558 }
559 })
560
561 t.Run("Sends update with custom timeout", func(t *testing.T) {
562 mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
563
564 translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
565 translator.Start()
566 defer translator.Stop()
567
568 translator.Update(profileWithTimeout)
569
570 actualPbProfile := <-mockGetProfileServer.profilesReceived
571 if !proto.Equal(actualPbProfile, pbProfileWithTimeout) {
572 t.Fatalf("Expected profile sent to be [%v] but was [%v]", pbProfileWithTimeout, actualPbProfile)
573 }
574 numProfiles := len(mockGetProfileServer.profilesReceived) + 1
575 if numProfiles != 1 {
576 t.Fatalf("Expecting [1] profile, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
577 }
578 })
579 }
580
View as plain text