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