1// +build !race
2
3// Copyright 2018 Istio 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
17package spybackend
18
19import (
20	"fmt"
21	"io/ioutil"
22	"strings"
23	"testing"
24	"time"
25
26	"github.com/gogo/protobuf/types"
27
28	rpc "istio.io/gogo-genproto/googleapis/google/rpc"
29
30	"istio.io/api/mixer/adapter/model/v1beta1"
31	istio_mixer_v1 "istio.io/api/mixer/v1"
32	policy_v1beta1 "istio.io/api/policy/v1beta1"
33	adapter_integration "istio.io/istio/mixer/pkg/adapter/test"
34	"istio.io/istio/mixer/pkg/status"
35	sampleapa "istio.io/istio/mixer/test/spyAdapter/template/apa"
36	checkproducer "istio.io/istio/mixer/test/spyAdapter/template/checkoutput"
37)
38
39const (
40	h1 = `
41apiVersion: "config.istio.io/v1alpha2"
42kind: handler
43metadata:
44  name: h1
45  namespace: istio-system
46spec:
47  adapter: spybackend-nosession
48  connection:
49    address: "%s"
50---
51`
52	i1Metric = `
53apiVersion: "config.istio.io/v1alpha2"
54kind: instance
55metadata:
56  name: i1metric
57  namespace: istio-system
58spec:
59  template: metric
60  params:
61    value: request.size | 123
62    dimensions:
63      destination_service: "\"unknown\""
64      response_code: "200"
65---
66`
67
68	r1H1I1Metric = `
69apiVersion: "config.istio.io/v1alpha2"
70kind: rule
71metadata:
72  name: r1
73  namespace: istio-system
74spec:
75  actions:
76  - handler: h1.istio-system
77    instances:
78    - i1metric
79---
80`
81
82	h2 = `
83apiVersion: "config.istio.io/v1alpha2"
84kind: handler
85metadata:
86  name: h2
87  namespace: istio-system
88spec:
89  adapter: spybackend-nosession
90  connection:
91    address: "%s"
92---
93`
94
95	i2Metric = `
96apiVersion: "config.istio.io/v1alpha2"
97kind: instance
98metadata:
99  name: i2metric
100  namespace: istio-system
101spec:
102  template: metric
103  params:
104    value: request.size | 456
105    dimensions:
106      destination_service: "\"unknown\""
107      response_code: "400"
108---
109`
110
111	r2H2I2Metric = `
112apiVersion: "config.istio.io/v1alpha2"
113kind: rule
114metadata:
115  name: r2
116  namespace: istio-system
117spec:
118  actions:
119  - handler: h2.istio-system
120    instances:
121    - i2metric
122---
123`
124	i3List = `
125apiVersion: "config.istio.io/v1alpha2"
126kind: instance
127metadata:
128  name: i3list
129  namespace: istio-system
130spec:
131  template: listentry
132  params:
133    value: source.name | "defaultstr"
134---
135`
136
137	r3H1I3List = `
138apiVersion: "config.istio.io/v1alpha2"
139kind: rule
140metadata:
141  name: r3
142  namespace: istio-system
143spec:
144  actions:
145  - handler: h1.istio-system
146    instances:
147    - i3list
148---
149`
150
151	i4Quota = `
152apiVersion: "config.istio.io/v1alpha2"
153kind: instance
154metadata:
155  name: requestQuota
156  namespace: istio-system
157spec:
158  template: quota
159  params:
160    dimensions:
161      source: source.labels["app"] | source.name | "unknown"
162      sourceVersion: source.labels["version"] | "unknown"
163      destination: destination.labels["app"] | destination.service.host | "unknown"
164      destinationVersion: destination.labels["version"] | "unknown"
165---
166`
167
168	r4h1i4Quota = `
169apiVersion: "config.istio.io/v1alpha2"
170kind: rule
171metadata:
172  name: r4
173  namespace: istio-system
174spec:
175  actions:
176  - handler: h1
177    instances:
178    - requestQuota
179---
180`
181
182	r6MatchIfReqIDH1i4Metric = `
183apiVersion: "config.istio.io/v1alpha2"
184kind: rule
185metadata:
186  name: r5
187  namespace: istio-system
188spec:
189  match: request.id | "unknown" != "unknown"
190  actions:
191  - handler: h1
192    instances:
193    - i1metric
194---
195`
196	i5Apa = `
197apiVersion: "config.istio.io/v1alpha2"
198kind: instance
199metadata:
200  name: genattrs
201  namespace: istio-system
202spec:
203  template: apa
204  params:
205    int64Primitive: request.size | 456
206  attribute_bindings:
207    request.size: output.int64Primitive
208---
209`
210
211	r7TriggerAPA = `
212apiVersion: "config.istio.io/v1alpha2"
213kind: rule
214metadata:
215  name: r7
216  namespace: istio-system
217spec:
218  match: (destination.namespace | "") == "trigger_apa"
219  actions:
220  - handler: h1
221    instances:
222    - genattrs
223---
224`
225
226	i6Checkoutput = `
227apiVersion: config.istio.io/v1alpha2
228kind: instance
229metadata:
230  name: i6
231  namespace: istio-system
232spec:
233  template: checkoutput
234  params:
235    stringPrimitive: destination.namespace | "unknown"
236`
237
238	r8Checkoutput = `
239apiVersion: config.istio.io/v1alpha2
240kind: rule
241metadata:
242  name: r7
243  namespace: istio-system
244spec:
245  actions:
246  - handler: h1
247    instances: ["i6"]
248    name: h1action
249  requestHeaderOperations:
250  - name: x-istio-test
251    values:
252    - h1action.output.stringPrimitive
253    - destination.namespace
254`
255)
256
257func TestNoSessionBackend(t *testing.T) {
258	testdata := []struct {
259		name   string
260		calls  []adapter_integration.Call
261		status rpc.Status
262		config []string
263		want   string
264	}{
265		{
266			name: "Check output call",
267			calls: []adapter_integration.Call{
268				{
269					CallKind: adapter_integration.CHECK,
270					Attrs:    map[string]interface{}{"destination.namespace": "testing-namespace"},
271				},
272			},
273			want: `
274{
275	"AdapterState": [],
276	"Returns": [
277	{
278	 "Check": {
279		"Status": {},
280		"ValidDuration": 5000000000,
281		"ValidUseCount": 31,
282		"RouteDirective": {
283			"request_header_operations": [
284				{
285					"name": "x-istio-test",
286					"value": "abracadabra"
287				},
288				{
289					"name": "x-istio-test",
290					"value": "testing-namespace"
291				}
292			],
293			"response_header_operations": null
294		}
295	 },
296	 "Quota": null,
297	 "Error": null
298	}
299	]
300}`,
301			config: []string{i6Checkoutput, r8Checkoutput},
302		},
303		{
304			// sets request.size to hardcoded value 1337
305			name: "APA call with attributes",
306			calls: []adapter_integration.Call{
307				{
308					CallKind: adapter_integration.REPORT,
309					Attrs:    map[string]interface{}{"destination.namespace": "trigger_apa"},
310				},
311			},
312			want: `
313						{
314						 "AdapterState": [
315						  {
316						   "dedup_id": "stripped_for_test",
317						   "instances": [
318						    {
319						     "dimensions": {
320						      "destination_service": {
321						       "stringValue": "unknown"
322						      },
323						      "response_code": {
324						       "int64Value": "400"
325						      }
326						     },
327						     "name": "i2metric.instance.istio-system",
328						     "value": {
329						      "int64Value": "1337"
330						     }
331						    }
332						   ]
333						  },
334						  {
335						   "dedup_id": "stripped_for_test",
336						   "instances": [
337						    {
338						     "dimensions": {
339						      "destination_service": {
340						       "stringValue": "unknown"
341						      },
342						      "response_code": {
343						       "int64Value": "200"
344						      }
345						     },
346						     "name": "i1metric.instance.istio-system",
347						     "value": {
348						      "int64Value": "1337"
349						     }
350						    }
351						   ]
352						  }
353						 ],
354						 "Returns": [
355						  {
356						   "Check": {
357						    "Status": {},
358						    "ValidDuration": 0,
359						    "ValidUseCount": 0
360						   },
361						   "Quota": null,
362						   "Error": null
363						  }
364						 ]
365						}
366				`,
367		},
368		{
369			name: "single report call with attributes",
370			calls: []adapter_integration.Call{
371				{
372					CallKind: adapter_integration.REPORT,
373					Attrs:    map[string]interface{}{"request.size": int64(666)},
374				},
375			},
376			want: `
377						{
378						 "AdapterState": [
379						  {
380						   "dedup_id": "stripped_for_test",
381						   "instances": [
382						    {
383						     "dimensions": {
384						      "destination_service": {
385						       "stringValue": "unknown"
386						      },
387						      "response_code": {
388						       "int64Value": "400"
389						      }
390						     },
391						     "name": "i2metric.instance.istio-system",
392						     "value": {
393						      "int64Value": "666"
394						     }
395						    }
396						   ]
397						  },
398						  {
399						   "dedup_id": "stripped_for_test",
400						   "instances": [
401						    {
402						     "dimensions": {
403						      "destination_service": {
404						       "stringValue": "unknown"
405						      },
406						      "response_code": {
407						       "int64Value": "200"
408						      }
409						     },
410						     "name": "i1metric.instance.istio-system",
411						     "value": {
412						      "int64Value": "666"
413						     }
414						    }
415						   ]
416						  }
417						 ],
418						 "Returns": [
419						  {
420						   "Check": {
421						    "Status": {},
422						    "ValidDuration": 0,
423						    "ValidUseCount": 0
424						   },
425						   "Quota": null,
426						   "Error": null
427						  }
428						 ]
429						}
430				`,
431		},
432		{
433			name: "single report call no attributes",
434			calls: []adapter_integration.Call{
435				{
436					CallKind: adapter_integration.REPORT,
437					Attrs:    map[string]interface{}{},
438				},
439			},
440			want: `
441							{
442							 "AdapterState": [
443							  {
444							   "dedup_id": "stripped_for_test",
445							   "instances": [
446							    {
447							     "dimensions": {
448							      "destination_service": {
449							       "stringValue": "unknown"
450							      },
451							      "response_code": {
452							       "int64Value": "400"
453							      }
454							     },
455							     "name": "i2metric.instance.istio-system",
456							     "value": {
457							      "int64Value": "456"
458							     }
459							    }
460							   ]
461							  },
462							  {
463							   "dedup_id": "stripped_for_test",
464							   "instances": [
465							    {
466							     "dimensions": {
467							      "destination_service": {
468							       "stringValue": "unknown"
469							      },
470							      "response_code": {
471							       "int64Value": "200"
472							      }
473							     },
474							     "name": "i1metric.instance.istio-system",
475							     "value": {
476							      "int64Value": "123"
477							     }
478							    }
479							   ]
480							  }
481							 ],
482							 "Returns": [
483							  {
484							   "Check": {
485							    "Status": {},
486							    "ValidDuration": 0,
487							    "ValidUseCount": 0
488							   },
489							   "Quota": null,
490							   "Error": null
491							  }
492							 ]
493							}
494					`,
495		},
496		{
497			name: "single check call with attributes",
498			calls: []adapter_integration.Call{
499				{
500					CallKind: adapter_integration.CHECK,
501					Attrs:    map[string]interface{}{"source.name": "foobar"},
502				},
503			},
504			want: `
505					   		{
506					    		 "AdapterState": [
507					    		  {
508					    		   "dedup_id": "stripped_for_test",
509					    		   "instance": {
510					    		    "name": "i3list.instance.istio-system",
511					    		    "value": {
512                                      "stringValue": "foobar"
513                                    }
514					    		   }
515					    		  }
516					    		 ],
517					    		 "Returns": [
518					    		  {
519					    		   "Check": {
520					    		    "Status": {},
521					    		    "ValidDuration": 0,
522					    		    "ValidUseCount": 31
523					    		   },
524					    		   "Quota": null,
525					    		   "Error": null
526					    		  }
527					    		 ]
528					    		}
529					`,
530		},
531		{
532			name: "single check call no attributes",
533			calls: []adapter_integration.Call{
534				{
535					CallKind: adapter_integration.CHECK,
536					Attrs:    map[string]interface{}{},
537				},
538			},
539			want: `
540					    		{
541					    		 "AdapterState": [
542					    		  {
543					    		   "dedup_id": "stripped_for_test",
544					    		   "instance": {
545					    		    "name": "i3list.instance.istio-system",
546					                "value": {
547                                      "stringValue": "defaultstr"
548                                    }
549					    		   }
550					    		  }
551					    		 ],
552					    		 "Returns": [
553					    		  {
554					    		   "Check": {
555					    		    "Status": {},
556					    		    "ValidDuration": 0,
557					    		    "ValidUseCount": 31
558					    		   },
559					    		   "Quota": null,
560					    		   "Error": null
561					    		  }
562					    		 ]
563					    		}
564					`,
565		},
566		{
567			name: "check custom error",
568			calls: []adapter_integration.Call{
569				{
570					CallKind: adapter_integration.CHECK,
571					Attrs:    map[string]interface{}{},
572				},
573			},
574			status: rpc.Status{
575				Code: int32(rpc.DATA_LOSS),
576				Details: []*types.Any{status.PackErrorDetail(&policy_v1beta1.DirectHttpResponse{
577					Code: policy_v1beta1.Unauthorized,
578					Body: "nope",
579				})},
580			},
581			want: `
582{
583    "AdapterState": [
584        {
585            "dedup_id": "stripped_for_test",
586            "instance": {
587                "name": "i3list.instance.istio-system",
588                "value": {
589                    "stringValue": "defaultstr"
590                }
591            }
592        }
593    ],
594    "Returns": [
595        {
596            "Check": {
597                "RouteDirective": {
598                    "direct_response_body": "nope",
599                    "direct_response_code": 401,
600                    "request_header_operations": null,
601                    "response_header_operations": null
602                },
603                "Status": {
604                    "code": 15,
605                    "message": "h1.handler.istio-system:"
606                },
607                "ValidDuration": 0,
608                "ValidUseCount": 31
609            },
610            "Error": null,
611            "Quota": null
612        }
613    ]
614}
615					`,
616		},
617		{
618			name: "single quota call with attributes",
619			calls: []adapter_integration.Call{{
620				CallKind: adapter_integration.CHECK,
621				Quotas: map[string]istio_mixer_v1.CheckRequest_QuotaParams{
622					"requestQuota": {
623						Amount:     35,
624						BestEffort: true,
625					},
626				},
627				Attrs: map[string]interface{}{"source.name": "foobar"},
628			}},
629			want: `
630					    		{
631					    		 "AdapterState": [
632					    		  {
633					    		   "dedup_id": "stripped_for_test",
634					    		   "instance": {
635					    		    "name": "i3list.instance.istio-system",
636					    		    "value": {
637                                      "stringValue": "foobar"
638                                    }
639					    		   }
640					    		  },
641					    		  {
642					    		   "dedup_id": "stripped_for_test",
643					    		   "instance": {
644					    		    "dimensions": {
645					    		     "destination": {
646					    		      "stringValue": "unknown"
647					    		     },
648					    		     "destinationVersion": {
649					    		      "stringValue": "unknown"
650					    		     },
651					    		     "source": {
652					    		      "stringValue": "foobar"
653					    		     },
654					    		     "sourceVersion": {
655					    		      "stringValue": "unknown"
656					    		     }
657					    		    },
658					    		    "name": "requestQuota.instance.istio-system"
659					    		   },
660					    		   "quota_request": {
661					    		    "quotas": {
662					    		     "requestQuota.instance.istio-system": {
663					    		      "amount": 35,
664					    		      "best_effort": true
665					    		     }
666					    		    }
667					    		   }
668					    		  }
669					    		 ],
670					    		 "Returns": [
671					    		  {
672					    		   "Check": {
673					    		    "Status": {},
674					    		    "ValidDuration": 0,
675					    		    "ValidUseCount": 0
676					    		   },
677					    		   "Quota": {
678					    		    "requestQuota": {
679					    		     "Status": {},
680					    		     "ValidDuration": 0,
681					    		     "Amount": 32
682					    		    }
683					    		   },
684					    		   "Error": null
685					    		  }
686					    		 ]
687					    		}
688					`,
689		},
690		{
691			name: "single quota call no attributes",
692			calls: []adapter_integration.Call{{
693				CallKind: adapter_integration.CHECK,
694				Quotas: map[string]istio_mixer_v1.CheckRequest_QuotaParams{
695					"requestQuota": {
696						Amount:     35,
697						BestEffort: true,
698					},
699				},
700			}},
701			want: `
702					    		{
703					    		 "AdapterState": [
704					    		  {
705					    		   "dedup_id": "stripped_for_test",
706					    		   "instance": {
707					    		    "name": "i3list.instance.istio-system",
708					    		    "value": {
709                                      "stringValue": "defaultstr"
710                                    }
711					    		   }
712					    		  },
713					    		  {
714					    		   "dedup_id": "stripped_for_test",
715					    		   "instance": {
716					    		    "dimensions": {
717					    		     "destination": {
718					    		      "stringValue": "unknown"
719					    		     },
720					    		     "destinationVersion": {
721					    		      "stringValue": "unknown"
722					    		     },
723					    		     "source": {
724					    		      "stringValue": "unknown"
725					    		     },
726					    		     "sourceVersion": {
727					    		      "stringValue": "unknown"
728					    		     }
729					    		    },
730					    		    "name": "requestQuota.instance.istio-system"
731					    		   },
732					    		   "quota_request": {
733					    		    "quotas": {
734					    		     "requestQuota.instance.istio-system": {
735					    		      "amount": 35,
736					    		      "best_effort": true
737					    		     }
738					    		    }
739					    		   }
740					    		  }
741					    		 ],
742					    		 "Returns": [
743					    		  {
744					    		   "Check": {
745					    		    "Status": {},
746					    		    "ValidDuration": 0,
747					    		    "ValidUseCount": 0
748					    		   },
749					    		   "Quota": {
750					    		    "requestQuota": {
751					    		     "Status": {},
752					    		     "ValidDuration": 0,
753					    		     "Amount": 32
754					    		    }
755					    		   },
756					    		   "Error": null
757					    		  }
758					    		 ]
759					    		}
760					`,
761		},
762
763		{
764			name: "multiple mix calls",
765			calls: []adapter_integration.Call{
766				// 3 report calls; varying request.size attribute and no attributes call too.
767				{
768					CallKind: adapter_integration.REPORT,
769					Attrs:    map[string]interface{}{"request.size": int64(666)},
770				},
771				{
772					CallKind: adapter_integration.REPORT,
773					Attrs:    map[string]interface{}{"request.size": int64(888)},
774				},
775				{
776					CallKind: adapter_integration.REPORT,
777				},
778
779				// 3 check calls; varying source.name attribute and no attributes call too.,
780				{
781					CallKind: adapter_integration.CHECK,
782					Attrs:    map[string]interface{}{"source.name": "foobar"},
783				},
784				{
785					CallKind: adapter_integration.CHECK,
786					Attrs:    map[string]interface{}{"source.name": "bazbaz"},
787				},
788				{
789					CallKind: adapter_integration.CHECK,
790				},
791
792				// one call with quota args
793				{
794					CallKind: adapter_integration.CHECK,
795					Quotas: map[string]istio_mixer_v1.CheckRequest_QuotaParams{
796						"requestQuota": {
797							Amount:     35,
798							BestEffort: true,
799						},
800					},
801				},
802				// one report request with request.id to match r4 rule
803				{
804					CallKind: adapter_integration.REPORT,
805					Attrs:    map[string]interface{}{"request.id": "somereqid"},
806				},
807			},
808
809			// want: --> multiple-mix-calls.golden.json
810			// * 4 i2metric.instance.istio-system for 4 report calls
811			// * 5 i1metric.instance.istio-system for 4 report calls (3 report calls without request.id attribute and 1 report calls
812			//     with request.id attribute, which result into 2 dispatch report rules to resolve successfully).
813			// * 4 i3list.instance.istio-system for 4 check calls
814			// * 1 requestQuota.instance.istio-system for 1 quota call
815		},
816	}
817
818	adptCfgBytes, err := ioutil.ReadFile("nosession.yaml")
819	if err != nil {
820		t.Fatalf("cannot open file: %v", err)
821	}
822
823	for _, td := range testdata {
824		t.Run(td.name, func(tt *testing.T) {
825			want := td.want
826			if want == "" {
827				want = readGoldenFile(tt, td.name)
828			}
829			adapter_integration.RunTest(
830				tt,
831				nil,
832				adapter_integration.Scenario{
833					Setup: func() (interface{}, error) {
834						args := DefaultArgs()
835						args.Behavior.HandleMetricResult = &v1beta1.ReportResult{}
836						args.Behavior.HandleListEntryResult = &v1beta1.CheckResult{
837							Status:        td.status,
838							ValidUseCount: 31,
839						}
840						args.Behavior.HandleQuotaResult = &v1beta1.QuotaResult{
841							Quotas: map[string]v1beta1.QuotaResult_Result{"requestQuota.instance.istio-system": {GrantedAmount: 32}}}
842						// populate the APA output with all values
843						args.Behavior.HandleSampleApaResult = &sampleapa.OutputMsg{
844							Int64Primitive:  1337,
845							BoolPrimitive:   true,
846							DoublePrimitive: 456.123,
847							StringPrimitive: "abracadabra",
848							StringMap:       map[string]string{"x": "y"},
849							Ip:              &policy_v1beta1.IPAddress{Value: []byte{127, 0, 0, 1}},
850							Duration:        &policy_v1beta1.Duration{Value: types.DurationProto(5 * time.Second)},
851							Timestamp:       &policy_v1beta1.TimeStamp{Value: types.TimestampNow()},
852							Dns:             &policy_v1beta1.DNSName{Value: "google.com"},
853						}
854						args.Behavior.HandleSampleCheckResult = &v1beta1.CheckResult{
855							ValidUseCount: 31,
856							ValidDuration: 5 * time.Second,
857						}
858						args.Behavior.HandleCheckOutput = &checkproducer.OutputMsg{
859							StringPrimitive: "abracadabra",
860						}
861
862						var s Server
863						var err error
864						if s, err = NewNoSessionServer(args); err != nil {
865							return nil, err
866						}
867						s.Run()
868						return s, nil
869					},
870					Teardown: func(ctx interface{}) {
871						_ = ctx.(Server).Close()
872					},
873					GetState: func(ctx interface{}) (interface{}, error) {
874						s := ctx.(*NoSessionServer)
875						return s.GetState(), nil
876					},
877					SingleThreaded: false,
878					ParallelCalls:  td.calls,
879					GetConfig: func(ctx interface{}) ([]string, error) {
880						s := ctx.(Server)
881
882						if td.config != nil {
883							return append(td.config,
884								// CRs for built-in templates are automatically added by the integration test framework.
885								string(adptCfgBytes), fmt.Sprintf(h1, s.Addr().String())), nil
886						}
887
888						return []string{
889							// CRs for built-in templates are automatically added by the integration test framework.
890							string(adptCfgBytes),
891							fmt.Sprintf(h1, s.Addr().String()),
892							i1Metric,
893							r1H1I1Metric,
894							fmt.Sprintf(h2, s.Addr().String()),
895							i2Metric,
896							r2H2I2Metric,
897							i3List,
898							r3H1I3List,
899							i4Quota,
900							r4h1i4Quota,
901							r6MatchIfReqIDH1i4Metric,
902							i5Apa,
903							r7TriggerAPA,
904						}, nil
905					},
906					Want: want,
907				},
908			)
909		})
910	}
911}
912
913// readGoldenFile reads contents based on the testname
914// "this is a test" --> "this-is-a-test.golden.json"
915func readGoldenFile(t *testing.T, testname string) string {
916	t.Helper()
917	filename := strings.Replace(testname, " ", "-", -1) + ".golden.json"
918	ba, err := ioutil.ReadFile(filename)
919	if err != nil {
920		t.Fatalf("unable to load verification file: %v", err)
921	}
922	return string(ba)
923}
924