istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/config/kube/gateway/conversion_test.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package gateway 16 17 import ( 18 "fmt" 19 "os" 20 "reflect" 21 "regexp" 22 "sort" 23 "strings" 24 "testing" 25 26 "github.com/google/go-cmp/cmp" 27 corev1 "k8s.io/api/core/v1" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/runtime" 30 k8s "sigs.k8s.io/gateway-api/apis/v1" 31 k8salpha "sigs.k8s.io/gateway-api/apis/v1alpha2" 32 "sigs.k8s.io/yaml" 33 34 istio "istio.io/api/networking/v1alpha3" 35 "istio.io/istio/pilot/pkg/config/kube/crd" 36 credentials "istio.io/istio/pilot/pkg/credentials/kube" 37 "istio.io/istio/pilot/pkg/features" 38 "istio.io/istio/pilot/pkg/model" 39 "istio.io/istio/pilot/pkg/model/kstatus" 40 "istio.io/istio/pilot/pkg/networking/core" 41 "istio.io/istio/pilot/test/util" 42 "istio.io/istio/pkg/cluster" 43 "istio.io/istio/pkg/config" 44 "istio.io/istio/pkg/config/constants" 45 crdvalidation "istio.io/istio/pkg/config/crd" 46 "istio.io/istio/pkg/config/schema/gvk" 47 "istio.io/istio/pkg/kube" 48 "istio.io/istio/pkg/test" 49 "istio.io/istio/pkg/test/util/assert" 50 "istio.io/istio/pkg/util/sets" 51 ) 52 53 var ports = []*model.Port{ 54 { 55 Name: "http", 56 Port: 80, 57 Protocol: "HTTP", 58 }, 59 { 60 Name: "tcp", 61 Port: 34000, 62 Protocol: "TCP", 63 }, 64 { 65 Name: "tcp-other", 66 Port: 34001, 67 Protocol: "TCP", 68 }, 69 } 70 71 var services = []*model.Service{ 72 { 73 Attributes: model.ServiceAttributes{ 74 Name: "istio-ingressgateway", 75 Namespace: "istio-system", 76 ClusterExternalAddresses: &model.AddressMap{ 77 Addresses: map[cluster.ID][]string{ 78 constants.DefaultClusterName: {"1.2.3.4"}, 79 }, 80 }, 81 }, 82 Ports: ports, 83 Hostname: "istio-ingressgateway.istio-system.svc.domain.suffix", 84 }, 85 { 86 Attributes: model.ServiceAttributes{ 87 Namespace: "istio-system", 88 }, 89 Ports: ports, 90 Hostname: "example.com", 91 }, 92 { 93 Attributes: model.ServiceAttributes{ 94 Namespace: "default", 95 }, 96 Ports: ports, 97 Hostname: "httpbin.default.svc.domain.suffix", 98 }, 99 { 100 Attributes: model.ServiceAttributes{ 101 Namespace: "apple", 102 }, 103 Ports: ports, 104 Hostname: "httpbin-apple.apple.svc.domain.suffix", 105 }, 106 { 107 Attributes: model.ServiceAttributes{ 108 Namespace: "banana", 109 }, 110 Ports: ports, 111 Hostname: "httpbin-banana.banana.svc.domain.suffix", 112 }, 113 { 114 Attributes: model.ServiceAttributes{ 115 Namespace: "default", 116 }, 117 Ports: ports, 118 Hostname: "httpbin-second.default.svc.domain.suffix", 119 }, 120 { 121 Attributes: model.ServiceAttributes{ 122 Namespace: "default", 123 }, 124 Ports: ports, 125 Hostname: "httpbin-wildcard.default.svc.domain.suffix", 126 }, 127 { 128 Attributes: model.ServiceAttributes{ 129 Namespace: "default", 130 }, 131 Ports: ports, 132 Hostname: "foo-svc.default.svc.domain.suffix", 133 }, 134 { 135 Attributes: model.ServiceAttributes{ 136 Namespace: "default", 137 }, 138 Ports: ports, 139 Hostname: "httpbin-other.default.svc.domain.suffix", 140 }, 141 { 142 Attributes: model.ServiceAttributes{ 143 Namespace: "default", 144 }, 145 Ports: ports, 146 Hostname: "example.default.svc.domain.suffix", 147 }, 148 { 149 Attributes: model.ServiceAttributes{ 150 Namespace: "default", 151 }, 152 Ports: ports, 153 Hostname: "echo.default.svc.domain.suffix", 154 }, 155 { 156 Attributes: model.ServiceAttributes{ 157 Namespace: "default", 158 }, 159 Ports: ports, 160 Hostname: "echo.default.svc.domain.suffix", 161 }, 162 { 163 Attributes: model.ServiceAttributes{ 164 Namespace: "cert", 165 }, 166 Ports: ports, 167 Hostname: "httpbin.cert.svc.domain.suffix", 168 }, 169 { 170 Attributes: model.ServiceAttributes{ 171 Namespace: "service", 172 }, 173 Ports: ports, 174 Hostname: "my-svc.service.svc.domain.suffix", 175 }, 176 { 177 Attributes: model.ServiceAttributes{ 178 Namespace: "default", 179 }, 180 Ports: ports, 181 Hostname: "google.com", 182 }, 183 { 184 Attributes: model.ServiceAttributes{ 185 Namespace: "allowed-1", 186 }, 187 Ports: ports, 188 Hostname: "svc2.allowed-1.svc.domain.suffix", 189 }, 190 { 191 Attributes: model.ServiceAttributes{ 192 Namespace: "allowed-2", 193 }, 194 Ports: ports, 195 Hostname: "svc2.allowed-2.svc.domain.suffix", 196 }, 197 { 198 Attributes: model.ServiceAttributes{ 199 Namespace: "allowed-1", 200 }, 201 Ports: ports, 202 Hostname: "svc1.allowed-1.svc.domain.suffix", 203 }, 204 { 205 Attributes: model.ServiceAttributes{ 206 Namespace: "allowed-2", 207 }, 208 Ports: ports, 209 Hostname: "svc3.allowed-2.svc.domain.suffix", 210 }, 211 { 212 Attributes: model.ServiceAttributes{ 213 Namespace: "default", 214 }, 215 Ports: ports, 216 Hostname: "svc4.default.svc.domain.suffix", 217 }, 218 { 219 Attributes: model.ServiceAttributes{ 220 Namespace: "group-namespace1", 221 }, 222 Ports: ports, 223 Hostname: "httpbin.group-namespace1.svc.domain.suffix", 224 }, 225 { 226 Attributes: model.ServiceAttributes{ 227 Namespace: "group-namespace2", 228 }, 229 Ports: ports, 230 Hostname: "httpbin.group-namespace2.svc.domain.suffix", 231 }, 232 { 233 Attributes: model.ServiceAttributes{ 234 Namespace: "default", 235 }, 236 Ports: ports, 237 Hostname: "httpbin-zero.default.svc.domain.suffix", 238 }, 239 { 240 Attributes: model.ServiceAttributes{ 241 Namespace: "istio-system", 242 }, 243 Ports: ports, 244 Hostname: "httpbin.istio-system.svc.domain.suffix", 245 }, 246 { 247 Attributes: model.ServiceAttributes{ 248 Namespace: "default", 249 }, 250 Ports: ports, 251 Hostname: "httpbin-mirror.default.svc.domain.suffix", 252 }, 253 { 254 Attributes: model.ServiceAttributes{ 255 Namespace: "default", 256 }, 257 Ports: ports, 258 Hostname: "httpbin-foo.default.svc.domain.suffix", 259 }, 260 { 261 Attributes: model.ServiceAttributes{ 262 Namespace: "default", 263 }, 264 Ports: ports, 265 Hostname: "httpbin-alt.default.svc.domain.suffix", 266 }, 267 { 268 Attributes: model.ServiceAttributes{ 269 Namespace: "istio-system", 270 }, 271 Ports: ports, 272 Hostname: "istiod.istio-system.svc.domain.suffix", 273 }, 274 { 275 Attributes: model.ServiceAttributes{ 276 Namespace: "istio-system", 277 }, 278 Ports: ports, 279 Hostname: "istiod.istio-system.svc.domain.suffix", 280 }, 281 { 282 Attributes: model.ServiceAttributes{ 283 Namespace: "istio-system", 284 }, 285 Ports: ports, 286 Hostname: "echo.istio-system.svc.domain.suffix", 287 }, 288 { 289 Attributes: model.ServiceAttributes{ 290 Namespace: "default", 291 }, 292 Ports: ports, 293 Hostname: "httpbin-bad.default.svc.domain.suffix", 294 }, 295 } 296 297 var ( 298 // https://github.com/kubernetes/kubernetes/blob/v1.25.4/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret_tls_test.go#L31 299 rsaCertPEM = `-----BEGIN CERTIFICATE----- 300 MIIB0zCCAX2gAwIBAgIJAI/M7BYjwB+uMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV 301 BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 302 aWRnaXRzIFB0eSBMdGQwHhcNMTIwOTEyMjE1MjAyWhcNMTUwOTEyMjE1MjAyWjBF 303 MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 304 ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANLJ 305 hPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wok/4xIA+ui35/MmNa 306 rtNuC+BdZ1tMuVCPFZcCAwEAAaNQME4wHQYDVR0OBBYEFJvKs8RfJaXTH08W+SGv 307 zQyKn0H8MB8GA1UdIwQYMBaAFJvKs8RfJaXTH08W+SGvzQyKn0H8MAwGA1UdEwQF 308 MAMBAf8wDQYJKoZIhvcNAQEFBQADQQBJlffJHybjDGxRMqaRmDhX0+6v02TUKZsW 309 r5QuVbpQhH6u+0UgcW0jp9QwpxoPTLTWGXEWBBBurxFwiCBhkQ+V 310 -----END CERTIFICATE----- 311 ` 312 rsaKeyPEM = `-----BEGIN RSA PRIVATE KEY----- 313 MIIBOwIBAAJBANLJhPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wo 314 k/4xIA+ui35/MmNartNuC+BdZ1tMuVCPFZcCAwEAAQJAEJ2N+zsR0Xn8/Q6twa4G 315 6OB1M1WO+k+ztnX/1SvNeWu8D6GImtupLTYgjZcHufykj09jiHmjHx8u8ZZB/o1N 316 MQIhAPW+eyZo7ay3lMz1V01WVjNKK9QSn1MJlb06h/LuYv9FAiEA25WPedKgVyCW 317 SmUwbPw8fnTcpqDWE3yTO3vKcebqMSsCIBF3UmVue8YU3jybC3NxuXq3wNm34R8T 318 xVLHwDXh/6NJAiEAl2oHGGLz64BuAfjKrqwz7qMYr9HCLIe/YsoWq/olzScCIQDi 319 D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNUN9B2N2g== 320 -----END RSA PRIVATE KEY----- 321 ` 322 323 secrets = []runtime.Object{ 324 &corev1.Secret{ 325 ObjectMeta: metav1.ObjectMeta{ 326 Name: "my-cert-http", 327 Namespace: "istio-system", 328 }, 329 Data: map[string][]byte{ 330 "tls.crt": []byte(rsaCertPEM), 331 "tls.key": []byte(rsaKeyPEM), 332 }, 333 }, 334 &corev1.Secret{ 335 ObjectMeta: metav1.ObjectMeta{ 336 Name: "cert", 337 Namespace: "cert", 338 }, 339 Data: map[string][]byte{ 340 "tls.crt": []byte(rsaCertPEM), 341 "tls.key": []byte(rsaKeyPEM), 342 }, 343 }, 344 &corev1.Secret{ 345 ObjectMeta: metav1.ObjectMeta{ 346 Name: "malformed", 347 Namespace: "istio-system", 348 }, 349 Data: map[string][]byte{ 350 // nolint: lll 351 // https://github.com/kubernetes-sigs/gateway-api/blob/d7f71d6b7df7e929ae299948973a693980afc183/conformance/tests/gateway-invalid-tls-certificateref.yaml#L87-L90 352 // this certificate is invalid because contains an invalid pem (base64 of "Hello world"), 353 // and the certificate and the key are identical 354 "tls.crt": []byte("SGVsbG8gd29ybGQK"), 355 "tls.key": []byte("SGVsbG8gd29ybGQK"), 356 }, 357 }, 358 } 359 ) 360 361 func init() { 362 features.EnableAlphaGatewayAPI = true 363 features.EnableAmbientWaypoints = true 364 // Recompute with ambient enabled 365 classInfos = getClassInfos() 366 builtinClasses = getBuiltinClasses() 367 } 368 369 func TestConvertResources(t *testing.T) { 370 validator := crdvalidation.NewIstioValidator(t) 371 cases := []struct { 372 name string 373 // Some configs are intended to be generated with invalid configs, and since they will be validated 374 // by the validator, we need to ignore the validation errors to prevent the test from failing. 375 validationIgnorer *crdvalidation.ValidationIgnorer 376 }{ 377 {name: "http"}, 378 {name: "tcp"}, 379 {name: "tls"}, 380 {name: "grpc"}, 381 {name: "mismatch"}, 382 {name: "weighted"}, 383 {name: "zero"}, 384 {name: "mesh"}, 385 { 386 name: "invalid", 387 validationIgnorer: crdvalidation.NewValidationIgnorer( 388 "default/^invalid-backendRef-kind-", 389 "default/^invalid-backendRef-mixed-", 390 ), 391 }, 392 {name: "multi-gateway"}, 393 {name: "delegated"}, 394 {name: "route-binding"}, 395 {name: "reference-policy-tls"}, 396 { 397 name: "reference-policy-service", 398 validationIgnorer: crdvalidation.NewValidationIgnorer( 399 "istio-system/^backend-not-allowed-", 400 ), 401 }, 402 { 403 name: "reference-policy-tcp", 404 validationIgnorer: crdvalidation.NewValidationIgnorer( 405 "istio-system/^not-allowed-echo-", 406 ), 407 }, 408 {name: "serviceentry"}, 409 {name: "eastwest"}, 410 {name: "eastwest-tlsoption"}, 411 {name: "eastwest-labelport"}, 412 {name: "eastwest-remote"}, 413 {name: "alias"}, 414 {name: "mcs"}, 415 {name: "route-precedence"}, 416 {name: "waypoint"}, 417 {name: "isolation"}, 418 } 419 for _, tt := range cases { 420 t.Run(tt.name, func(t *testing.T) { 421 input := readConfig(t, fmt.Sprintf("testdata/%s.yaml", tt.name), validator, nil) 422 // Setup a few preconfigured services 423 instances := []*model.ServiceInstance{} 424 for _, svc := range services { 425 instances = append(instances, &model.ServiceInstance{ 426 Service: svc, 427 ServicePort: ports[0], 428 Endpoint: &model.IstioEndpoint{EndpointPort: 8080}, 429 }, &model.ServiceInstance{ 430 Service: svc, 431 ServicePort: ports[1], 432 Endpoint: &model.IstioEndpoint{}, 433 }, &model.ServiceInstance{ 434 Service: svc, 435 ServicePort: ports[2], 436 Endpoint: &model.IstioEndpoint{}, 437 }) 438 } 439 cg := core.NewConfigGenTest(t, core.TestOptions{ 440 Services: services, 441 Instances: instances, 442 }) 443 kr := splitInput(t, input) 444 kr.Context = NewGatewayContext(cg.PushContext(), "Kubernetes") 445 output := convertResources(kr) 446 output.AllowedReferences = AllowedReferences{} // Not tested here 447 output.ReferencedNamespaceKeys = nil // Not tested here 448 output.ResourceReferences = nil // Not tested here 449 450 // sort virtual services to make the order deterministic 451 sort.Slice(output.VirtualService, func(i, j int) bool { 452 return output.VirtualService[i].Namespace+"/"+output.VirtualService[i].Name < output.VirtualService[j].Namespace+"/"+output.VirtualService[j].Name 453 }) 454 goldenFile := fmt.Sprintf("testdata/%s.yaml.golden", tt.name) 455 res := append(output.Gateway, output.VirtualService...) 456 util.CompareContent(t, marshalYaml(t, res), goldenFile) 457 golden := splitOutput(readConfig(t, goldenFile, validator, tt.validationIgnorer)) 458 459 // sort virtual services to make the order deterministic 460 sort.Slice(golden.VirtualService, func(i, j int) bool { 461 return golden.VirtualService[i].Namespace+"/"+golden.VirtualService[i].Name < golden.VirtualService[j].Namespace+"/"+golden.VirtualService[j].Name 462 }) 463 464 assert.Equal(t, golden, output) 465 466 outputStatus := getStatus(t, kr.GatewayClass, kr.Gateway, kr.HTTPRoute, kr.GRPCRoute, kr.TLSRoute, kr.TCPRoute) 467 goldenStatusFile := fmt.Sprintf("testdata/%s.status.yaml.golden", tt.name) 468 if util.Refresh() { 469 if err := os.WriteFile(goldenStatusFile, outputStatus, 0o644); err != nil { 470 t.Fatal(err) 471 } 472 } 473 goldenStatus, err := os.ReadFile(goldenStatusFile) 474 if err != nil { 475 t.Fatal(err) 476 } 477 if diff := cmp.Diff(string(goldenStatus), string(outputStatus)); diff != "" { 478 t.Fatalf("Diff:\n%s", diff) 479 } 480 }) 481 } 482 } 483 484 func TestSortHTTPRoutes(t *testing.T) { 485 cases := []struct { 486 name string 487 in []*istio.HTTPRoute 488 out []*istio.HTTPRoute 489 }{ 490 { 491 "match is preferred over no match", 492 []*istio.HTTPRoute{ 493 { 494 Match: []*istio.HTTPMatchRequest{}, 495 }, 496 { 497 Match: []*istio.HTTPMatchRequest{ 498 { 499 Uri: &istio.StringMatch{ 500 MatchType: &istio.StringMatch_Exact{ 501 Exact: "/foo", 502 }, 503 }, 504 }, 505 }, 506 }, 507 }, 508 []*istio.HTTPRoute{ 509 { 510 Match: []*istio.HTTPMatchRequest{ 511 { 512 Uri: &istio.StringMatch{ 513 MatchType: &istio.StringMatch_Exact{ 514 Exact: "/foo", 515 }, 516 }, 517 }, 518 }, 519 }, 520 { 521 Match: []*istio.HTTPMatchRequest{}, 522 }, 523 }, 524 }, 525 { 526 "path matching exact > prefix > regex", 527 []*istio.HTTPRoute{ 528 { 529 Match: []*istio.HTTPMatchRequest{ 530 { 531 Uri: &istio.StringMatch{ 532 MatchType: &istio.StringMatch_Prefix{ 533 Prefix: "/", 534 }, 535 }, 536 }, 537 }, 538 }, 539 { 540 Match: []*istio.HTTPMatchRequest{ 541 { 542 Uri: &istio.StringMatch{ 543 MatchType: &istio.StringMatch_Regex{ 544 Regex: ".*foo", 545 }, 546 }, 547 }, 548 }, 549 }, 550 { 551 Match: []*istio.HTTPMatchRequest{ 552 { 553 Uri: &istio.StringMatch{ 554 MatchType: &istio.StringMatch_Exact{ 555 Exact: "/foo", 556 }, 557 }, 558 }, 559 }, 560 }, 561 }, 562 []*istio.HTTPRoute{ 563 { 564 Match: []*istio.HTTPMatchRequest{ 565 { 566 Uri: &istio.StringMatch{ 567 MatchType: &istio.StringMatch_Exact{ 568 Exact: "/foo", 569 }, 570 }, 571 }, 572 }, 573 }, 574 { 575 Match: []*istio.HTTPMatchRequest{ 576 { 577 Uri: &istio.StringMatch{ 578 MatchType: &istio.StringMatch_Prefix{ 579 Prefix: "/", 580 }, 581 }, 582 }, 583 }, 584 }, 585 { 586 Match: []*istio.HTTPMatchRequest{ 587 { 588 Uri: &istio.StringMatch{ 589 MatchType: &istio.StringMatch_Regex{ 590 Regex: ".*foo", 591 }, 592 }, 593 }, 594 }, 595 }, 596 }, 597 }, 598 { 599 "path prefix matching with largest characters", 600 []*istio.HTTPRoute{ 601 { 602 Match: []*istio.HTTPMatchRequest{ 603 { 604 Uri: &istio.StringMatch{ 605 MatchType: &istio.StringMatch_Prefix{ 606 Prefix: "/foo", 607 }, 608 }, 609 }, 610 }, 611 }, 612 { 613 Match: []*istio.HTTPMatchRequest{ 614 { 615 Uri: &istio.StringMatch{ 616 MatchType: &istio.StringMatch_Prefix{ 617 Prefix: "/", 618 }, 619 }, 620 }, 621 }, 622 }, 623 { 624 Match: []*istio.HTTPMatchRequest{ 625 { 626 Uri: &istio.StringMatch{ 627 MatchType: &istio.StringMatch_Prefix{ 628 Prefix: "/foobar", 629 }, 630 }, 631 }, 632 }, 633 }, 634 }, 635 []*istio.HTTPRoute{ 636 { 637 Match: []*istio.HTTPMatchRequest{ 638 { 639 Uri: &istio.StringMatch{ 640 MatchType: &istio.StringMatch_Prefix{ 641 Prefix: "/foobar", 642 }, 643 }, 644 }, 645 }, 646 }, 647 { 648 Match: []*istio.HTTPMatchRequest{ 649 { 650 Uri: &istio.StringMatch{ 651 MatchType: &istio.StringMatch_Prefix{ 652 Prefix: "/foo", 653 }, 654 }, 655 }, 656 }, 657 }, 658 { 659 Match: []*istio.HTTPMatchRequest{ 660 { 661 Uri: &istio.StringMatch{ 662 MatchType: &istio.StringMatch_Prefix{ 663 Prefix: "/", 664 }, 665 }, 666 }, 667 }, 668 }, 669 }, 670 }, 671 { 672 "path match is preferred over method match", 673 []*istio.HTTPRoute{ 674 { 675 Match: []*istio.HTTPMatchRequest{ 676 { 677 Method: &istio.StringMatch{ 678 MatchType: &istio.StringMatch_Exact{ 679 Exact: "GET", 680 }, 681 }, 682 }, 683 }, 684 }, 685 { 686 Match: []*istio.HTTPMatchRequest{ 687 { 688 Uri: &istio.StringMatch{ 689 MatchType: &istio.StringMatch_Prefix{ 690 Prefix: "/foobar", 691 }, 692 }, 693 }, 694 }, 695 }, 696 }, 697 []*istio.HTTPRoute{ 698 { 699 Match: []*istio.HTTPMatchRequest{ 700 { 701 Uri: &istio.StringMatch{ 702 MatchType: &istio.StringMatch_Prefix{ 703 Prefix: "/foobar", 704 }, 705 }, 706 }, 707 }, 708 }, 709 { 710 Match: []*istio.HTTPMatchRequest{ 711 { 712 Method: &istio.StringMatch{ 713 MatchType: &istio.StringMatch_Exact{ 714 Exact: "GET", 715 }, 716 }, 717 }, 718 }, 719 }, 720 }, 721 }, 722 { 723 "largest number of header matches is preferred", 724 []*istio.HTTPRoute{ 725 { 726 Match: []*istio.HTTPMatchRequest{ 727 { 728 Headers: map[string]*istio.StringMatch{ 729 "header1": { 730 MatchType: &istio.StringMatch_Exact{ 731 Exact: "value1", 732 }, 733 }, 734 }, 735 }, 736 }, 737 }, 738 { 739 Match: []*istio.HTTPMatchRequest{ 740 { 741 Headers: map[string]*istio.StringMatch{ 742 "header1": { 743 MatchType: &istio.StringMatch_Exact{ 744 Exact: "value1", 745 }, 746 }, 747 "header2": { 748 MatchType: &istio.StringMatch_Exact{ 749 Exact: "value2", 750 }, 751 }, 752 }, 753 }, 754 }, 755 }, 756 }, 757 []*istio.HTTPRoute{ 758 { 759 Match: []*istio.HTTPMatchRequest{ 760 { 761 Headers: map[string]*istio.StringMatch{ 762 "header1": { 763 MatchType: &istio.StringMatch_Exact{ 764 Exact: "value1", 765 }, 766 }, 767 "header2": { 768 MatchType: &istio.StringMatch_Exact{ 769 Exact: "value2", 770 }, 771 }, 772 }, 773 }, 774 }, 775 }, 776 { 777 Match: []*istio.HTTPMatchRequest{ 778 { 779 Headers: map[string]*istio.StringMatch{ 780 "header1": { 781 MatchType: &istio.StringMatch_Exact{ 782 Exact: "value1", 783 }, 784 }, 785 }, 786 }, 787 }, 788 }, 789 }, 790 }, 791 { 792 "largest number of query params is preferred", 793 []*istio.HTTPRoute{ 794 { 795 Match: []*istio.HTTPMatchRequest{ 796 { 797 QueryParams: map[string]*istio.StringMatch{ 798 "param1": { 799 MatchType: &istio.StringMatch_Exact{ 800 Exact: "value1", 801 }, 802 }, 803 }, 804 }, 805 }, 806 }, 807 { 808 Match: []*istio.HTTPMatchRequest{ 809 { 810 QueryParams: map[string]*istio.StringMatch{ 811 "param1": { 812 MatchType: &istio.StringMatch_Exact{ 813 Exact: "value1", 814 }, 815 }, 816 "param2": { 817 MatchType: &istio.StringMatch_Exact{ 818 Exact: "value2", 819 }, 820 }, 821 }, 822 }, 823 }, 824 }, 825 }, 826 []*istio.HTTPRoute{ 827 { 828 Match: []*istio.HTTPMatchRequest{ 829 { 830 QueryParams: map[string]*istio.StringMatch{ 831 "param1": { 832 MatchType: &istio.StringMatch_Exact{ 833 Exact: "value1", 834 }, 835 }, 836 "param2": { 837 MatchType: &istio.StringMatch_Exact{ 838 Exact: "value2", 839 }, 840 }, 841 }, 842 }, 843 }, 844 }, 845 { 846 Match: []*istio.HTTPMatchRequest{ 847 { 848 QueryParams: map[string]*istio.StringMatch{ 849 "param1": { 850 MatchType: &istio.StringMatch_Exact{ 851 Exact: "value1", 852 }, 853 }, 854 }, 855 }, 856 }, 857 }, 858 }, 859 }, 860 { 861 "path > method > header > query params", 862 []*istio.HTTPRoute{ 863 { 864 Match: []*istio.HTTPMatchRequest{ 865 { 866 Uri: &istio.StringMatch{ 867 MatchType: &istio.StringMatch_Prefix{ 868 Prefix: "/", 869 }, 870 }, 871 }, 872 }, 873 }, 874 { 875 Match: []*istio.HTTPMatchRequest{ 876 { 877 QueryParams: map[string]*istio.StringMatch{ 878 "param1": { 879 MatchType: &istio.StringMatch_Exact{ 880 Exact: "value1", 881 }, 882 }, 883 }, 884 }, 885 }, 886 }, 887 { 888 Match: []*istio.HTTPMatchRequest{ 889 { 890 Method: &istio.StringMatch{ 891 MatchType: &istio.StringMatch_Exact{Exact: "GET"}, 892 }, 893 }, 894 }, 895 }, 896 { 897 Match: []*istio.HTTPMatchRequest{ 898 { 899 Headers: map[string]*istio.StringMatch{ 900 "param1": { 901 MatchType: &istio.StringMatch_Exact{ 902 Exact: "value1", 903 }, 904 }, 905 }, 906 }, 907 }, 908 }, 909 }, 910 []*istio.HTTPRoute{ 911 { 912 Match: []*istio.HTTPMatchRequest{ 913 { 914 Uri: &istio.StringMatch{ 915 MatchType: &istio.StringMatch_Prefix{ 916 Prefix: "/", 917 }, 918 }, 919 }, 920 }, 921 }, 922 { 923 Match: []*istio.HTTPMatchRequest{ 924 { 925 Method: &istio.StringMatch{ 926 MatchType: &istio.StringMatch_Exact{Exact: "GET"}, 927 }, 928 }, 929 }, 930 }, 931 { 932 Match: []*istio.HTTPMatchRequest{ 933 { 934 Headers: map[string]*istio.StringMatch{ 935 "param1": { 936 MatchType: &istio.StringMatch_Exact{ 937 Exact: "value1", 938 }, 939 }, 940 }, 941 }, 942 }, 943 }, 944 { 945 Match: []*istio.HTTPMatchRequest{ 946 { 947 QueryParams: map[string]*istio.StringMatch{ 948 "param1": { 949 MatchType: &istio.StringMatch_Exact{ 950 Exact: "value1", 951 }, 952 }, 953 }, 954 }, 955 }, 956 }, 957 }, 958 }, 959 } 960 961 for _, tt := range cases { 962 t.Run(tt.name, func(t *testing.T) { 963 sortHTTPRoutes(tt.in) 964 if !reflect.DeepEqual(tt.in, tt.out) { 965 t.Fatalf("expected %v, got %v", tt.out, tt.in) 966 } 967 }) 968 } 969 } 970 971 func TestReferencePolicy(t *testing.T) { 972 validator := crdvalidation.NewIstioValidator(t) 973 type res struct { 974 name, namespace string 975 allowed bool 976 } 977 cases := []struct { 978 name string 979 config string 980 expectations []res 981 }{ 982 { 983 name: "simple", 984 config: `apiVersion: gateway.networking.k8s.io/v1beta1 985 kind: ReferenceGrant 986 metadata: 987 name: allow-gateways-to-ref-secrets 988 namespace: default 989 spec: 990 from: 991 - group: gateway.networking.k8s.io 992 kind: Gateway 993 namespace: istio-system 994 to: 995 - group: "" 996 kind: Secret 997 `, 998 expectations: []res{ 999 // allow cross namespace 1000 {"kubernetes-gateway://default/wildcard-example-com-cert", "istio-system", true}, 1001 // denied same namespace. We do not implicitly allow (in this code - higher level code does) 1002 {"kubernetes-gateway://default/wildcard-example-com-cert", "default", false}, 1003 // denied namespace 1004 {"kubernetes-gateway://default/wildcard-example-com-cert", "bad", false}, 1005 }, 1006 }, 1007 { 1008 name: "multiple in one", 1009 config: `apiVersion: gateway.networking.k8s.io/v1beta1 1010 kind: ReferenceGrant 1011 metadata: 1012 name: allow-gateways-to-ref-secrets 1013 namespace: default 1014 spec: 1015 from: 1016 - group: gateway.networking.k8s.io 1017 kind: Gateway 1018 namespace: ns-1 1019 - group: gateway.networking.k8s.io 1020 kind: Gateway 1021 namespace: ns-2 1022 to: 1023 - group: "" 1024 kind: Secret 1025 `, 1026 expectations: []res{ 1027 {"kubernetes-gateway://default/wildcard-example-com-cert", "ns-1", true}, 1028 {"kubernetes-gateway://default/wildcard-example-com-cert", "ns-2", true}, 1029 {"kubernetes-gateway://default/wildcard-example-com-cert", "bad", false}, 1030 }, 1031 }, 1032 { 1033 name: "multiple", 1034 config: `apiVersion: gateway.networking.k8s.io/v1beta1 1035 kind: ReferenceGrant 1036 metadata: 1037 name: ns1 1038 namespace: default 1039 spec: 1040 from: 1041 - group: gateway.networking.k8s.io 1042 kind: Gateway 1043 namespace: ns-1 1044 to: 1045 - group: "" 1046 kind: Secret 1047 --- 1048 apiVersion: gateway.networking.k8s.io/v1beta1 1049 kind: ReferenceGrant 1050 metadata: 1051 name: ns2 1052 namespace: default 1053 spec: 1054 from: 1055 - group: gateway.networking.k8s.io 1056 kind: Gateway 1057 namespace: ns-2 1058 to: 1059 - group: "" 1060 kind: Secret 1061 `, 1062 expectations: []res{ 1063 {"kubernetes-gateway://default/wildcard-example-com-cert", "ns-1", true}, 1064 {"kubernetes-gateway://default/wildcard-example-com-cert", "ns-2", true}, 1065 {"kubernetes-gateway://default/wildcard-example-com-cert", "bad", false}, 1066 }, 1067 }, 1068 { 1069 name: "same namespace", 1070 config: `apiVersion: gateway.networking.k8s.io/v1beta1 1071 kind: ReferenceGrant 1072 metadata: 1073 name: allow-gateways-to-ref-secrets 1074 namespace: default 1075 spec: 1076 from: 1077 - group: gateway.networking.k8s.io 1078 kind: Gateway 1079 namespace: default 1080 to: 1081 - group: "" 1082 kind: Secret 1083 `, 1084 expectations: []res{ 1085 {"kubernetes-gateway://default/wildcard-example-com-cert", "istio-system", false}, 1086 {"kubernetes-gateway://default/wildcard-example-com-cert", "default", true}, 1087 {"kubernetes-gateway://default/wildcard-example-com-cert", "bad", false}, 1088 }, 1089 }, 1090 { 1091 name: "same name", 1092 config: `apiVersion: gateway.networking.k8s.io/v1beta1 1093 kind: ReferenceGrant 1094 metadata: 1095 name: allow-gateways-to-ref-secrets 1096 namespace: default 1097 spec: 1098 from: 1099 - group: gateway.networking.k8s.io 1100 kind: Gateway 1101 namespace: default 1102 to: 1103 - group: "" 1104 kind: Secret 1105 name: public 1106 `, 1107 expectations: []res{ 1108 {"kubernetes-gateway://default/public", "istio-system", false}, 1109 {"kubernetes-gateway://default/public", "default", true}, 1110 {"kubernetes-gateway://default/private", "default", false}, 1111 }, 1112 }, 1113 } 1114 for _, tt := range cases { 1115 t.Run(tt.name, func(t *testing.T) { 1116 input := readConfigString(t, tt.config, validator, nil) 1117 cg := core.NewConfigGenTest(t, core.TestOptions{}) 1118 kr := splitInput(t, input) 1119 kr.Context = NewGatewayContext(cg.PushContext(), "Kubernetes") 1120 output := convertResources(kr) 1121 c := &Controller{ 1122 state: output, 1123 } 1124 for _, sc := range tt.expectations { 1125 t.Run(fmt.Sprintf("%v/%v", sc.name, sc.namespace), func(t *testing.T) { 1126 got := c.SecretAllowed(sc.name, sc.namespace) 1127 if got != sc.allowed { 1128 t.Fatalf("expected allowed=%v, got allowed=%v", sc.allowed, got) 1129 } 1130 }) 1131 } 1132 }) 1133 } 1134 } 1135 1136 func getStatus(t test.Failer, acfgs ...[]config.Config) []byte { 1137 cfgs := []config.Config{} 1138 for _, cl := range acfgs { 1139 cfgs = append(cfgs, cl...) 1140 } 1141 for i, c := range cfgs { 1142 if c.Status.(*kstatus.WrappedStatus) != nil && c.GroupVersionKind == gvk.GatewayClass { 1143 // Override GatewaySupportedFeatures for the test so we dont have huge golden files plus we wont need to update them every time we support a new feature 1144 c.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status { 1145 gcs := s.(*k8s.GatewayClassStatus) 1146 gcs.SupportedFeatures = []k8s.SupportedFeature{"HTTPRouteFeatureA", "HTTPRouteFeatureB"} 1147 return gcs 1148 }) 1149 } 1150 c = c.DeepCopy() 1151 c.Spec = nil 1152 c.Labels = nil 1153 c.Annotations = nil 1154 if c.Status.(*kstatus.WrappedStatus) != nil { 1155 c.Status = c.Status.(*kstatus.WrappedStatus).Status 1156 } 1157 cfgs[i] = c 1158 } 1159 return timestampRegex.ReplaceAll(marshalYaml(t, cfgs), []byte("lastTransitionTime: fake")) 1160 } 1161 1162 var timestampRegex = regexp.MustCompile(`lastTransitionTime:.*`) 1163 1164 func splitOutput(configs []config.Config) IstioResources { 1165 out := IstioResources{ 1166 Gateway: []config.Config{}, 1167 VirtualService: []config.Config{}, 1168 } 1169 for _, c := range configs { 1170 c.Domain = "domain.suffix" 1171 switch c.GroupVersionKind { 1172 case gvk.Gateway: 1173 out.Gateway = append(out.Gateway, c) 1174 case gvk.VirtualService: 1175 out.VirtualService = append(out.VirtualService, c) 1176 } 1177 } 1178 return out 1179 } 1180 1181 func splitInput(t test.Failer, configs []config.Config) GatewayResources { 1182 out := GatewayResources{} 1183 namespaces := sets.New[string]() 1184 for _, c := range configs { 1185 namespaces.Insert(c.Namespace) 1186 switch c.GroupVersionKind { 1187 case gvk.GatewayClass: 1188 out.GatewayClass = append(out.GatewayClass, c) 1189 case gvk.KubernetesGateway: 1190 out.Gateway = append(out.Gateway, c) 1191 case gvk.HTTPRoute: 1192 out.HTTPRoute = append(out.HTTPRoute, c) 1193 case gvk.GRPCRoute: 1194 out.GRPCRoute = append(out.GRPCRoute, c) 1195 case gvk.TCPRoute: 1196 out.TCPRoute = append(out.TCPRoute, c) 1197 case gvk.TLSRoute: 1198 out.TLSRoute = append(out.TLSRoute, c) 1199 case gvk.ReferenceGrant: 1200 out.ReferenceGrant = append(out.ReferenceGrant, c) 1201 case gvk.ServiceEntry: 1202 out.ServiceEntry = append(out.ServiceEntry, c) 1203 } 1204 } 1205 out.Namespaces = map[string]*corev1.Namespace{} 1206 for ns := range namespaces { 1207 out.Namespaces[ns] = &corev1.Namespace{ 1208 ObjectMeta: metav1.ObjectMeta{ 1209 Name: ns, 1210 Labels: map[string]string{ 1211 "istio.io/test-name-part": strings.Split(ns, "-")[0], 1212 }, 1213 }, 1214 } 1215 } 1216 1217 client := kube.NewFakeClient(secrets...) 1218 out.Credentials = credentials.NewCredentialsController(client, nil) 1219 client.RunAndWait(test.NewStop(t)) 1220 1221 out.Domain = "domain.suffix" 1222 return out 1223 } 1224 1225 func readConfig(t testing.TB, filename string, validator *crdvalidation.Validator, ignorer *crdvalidation.ValidationIgnorer) []config.Config { 1226 t.Helper() 1227 1228 data, err := os.ReadFile(filename) 1229 if err != nil { 1230 t.Fatalf("failed to read input yaml file: %v", err) 1231 } 1232 return readConfigString(t, string(data), validator, ignorer) 1233 } 1234 1235 func readConfigString(t testing.TB, data string, validator *crdvalidation.Validator, ignorer *crdvalidation.ValidationIgnorer, 1236 ) []config.Config { 1237 if err := validator.ValidateCustomResourceYAML(data, ignorer); err != nil { 1238 t.Error(err) 1239 } 1240 c, _, err := crd.ParseInputs(data) 1241 if err != nil { 1242 t.Fatalf("failed to parse CRD: %v", err) 1243 } 1244 return insertDefaults(c) 1245 } 1246 1247 // insertDefaults sets default values that would be present when reading from Kubernetes but not from 1248 // files 1249 func insertDefaults(cfgs []config.Config) []config.Config { 1250 res := make([]config.Config, 0, len(cfgs)) 1251 for _, c := range cfgs { 1252 switch c.GroupVersionKind { 1253 case gvk.GatewayClass: 1254 c.Status = kstatus.Wrap(&k8s.GatewayClassStatus{}) 1255 case gvk.KubernetesGateway: 1256 c.Status = kstatus.Wrap(&k8s.GatewayStatus{}) 1257 case gvk.HTTPRoute: 1258 c.Status = kstatus.Wrap(&k8s.HTTPRouteStatus{}) 1259 case gvk.GRPCRoute: 1260 c.Status = kstatus.Wrap(&k8s.GRPCRouteStatus{}) 1261 case gvk.TCPRoute: 1262 c.Status = kstatus.Wrap(&k8salpha.TCPRouteStatus{}) 1263 case gvk.TLSRoute: 1264 c.Status = kstatus.Wrap(&k8salpha.TLSRouteStatus{}) 1265 } 1266 res = append(res, c) 1267 } 1268 return res 1269 } 1270 1271 // Print as YAML 1272 func marshalYaml(t test.Failer, cl []config.Config) []byte { 1273 t.Helper() 1274 result := []byte{} 1275 separator := []byte("---\n") 1276 for _, config := range cl { 1277 obj, err := crd.ConvertConfig(config) 1278 if err != nil { 1279 t.Fatalf("Could not decode %v: %v", config.Name, err) 1280 } 1281 bytes, err := yaml.Marshal(obj) 1282 if err != nil { 1283 t.Fatalf("Could not convert %v to YAML: %v", config, err) 1284 } 1285 result = append(result, bytes...) 1286 result = append(result, separator...) 1287 } 1288 return result 1289 } 1290 1291 func TestHumanReadableJoin(t *testing.T) { 1292 tests := []struct { 1293 input []string 1294 want string 1295 }{ 1296 {[]string{"a"}, "a"}, 1297 {[]string{"a", "b"}, "a and b"}, 1298 {[]string{"a", "b", "c"}, "a, b, and c"}, 1299 } 1300 for _, tt := range tests { 1301 t.Run(strings.Join(tt.input, "_"), func(t *testing.T) { 1302 if got := humanReadableJoin(tt.input); !reflect.DeepEqual(got, tt.want) { 1303 t.Errorf("got %v, want %v", got, tt.want) 1304 } 1305 }) 1306 } 1307 } 1308 1309 func BenchmarkBuildHTTPVirtualServices(b *testing.B) { 1310 ports := []*model.Port{ 1311 { 1312 Name: "http", 1313 Port: 80, 1314 Protocol: "HTTP", 1315 }, 1316 { 1317 Name: "tcp", 1318 Port: 34000, 1319 Protocol: "TCP", 1320 }, 1321 } 1322 ingressSvc := &model.Service{ 1323 Attributes: model.ServiceAttributes{ 1324 Name: "istio-ingressgateway", 1325 Namespace: "istio-system", 1326 ClusterExternalAddresses: &model.AddressMap{ 1327 Addresses: map[cluster.ID][]string{ 1328 constants.DefaultClusterName: {"1.2.3.4"}, 1329 }, 1330 }, 1331 }, 1332 Ports: ports, 1333 Hostname: "istio-ingressgateway.istio-system.svc.domain.suffix", 1334 } 1335 altIngressSvc := &model.Service{ 1336 Attributes: model.ServiceAttributes{ 1337 Namespace: "istio-system", 1338 }, 1339 Ports: ports, 1340 Hostname: "example.com", 1341 } 1342 cg := core.NewConfigGenTest(b, core.TestOptions{ 1343 Services: []*model.Service{ingressSvc, altIngressSvc}, 1344 Instances: []*model.ServiceInstance{ 1345 {Service: ingressSvc, ServicePort: ingressSvc.Ports[0], Endpoint: &model.IstioEndpoint{EndpointPort: 8080}}, 1346 {Service: ingressSvc, ServicePort: ingressSvc.Ports[1], Endpoint: &model.IstioEndpoint{}}, 1347 {Service: altIngressSvc, ServicePort: altIngressSvc.Ports[0], Endpoint: &model.IstioEndpoint{}}, 1348 {Service: altIngressSvc, ServicePort: altIngressSvc.Ports[1], Endpoint: &model.IstioEndpoint{}}, 1349 }, 1350 }) 1351 1352 validator := crdvalidation.NewIstioValidator(b) 1353 input := readConfig(b, "testdata/benchmark-httproute.yaml", validator, nil) 1354 kr := splitInput(b, input) 1355 kr.Context = NewGatewayContext(cg.PushContext(), "Kubernetes") 1356 ctx := configContext{ 1357 GatewayResources: kr, 1358 AllowedReferences: convertReferencePolicies(kr), 1359 } 1360 _, gwMap, _ := convertGateways(ctx) 1361 ctx.GatewayReferences = gwMap 1362 1363 b.ResetTimer() 1364 for n := 0; n < b.N; n++ { 1365 // for gateway routes, build one VS per gateway+host 1366 gatewayRoutes := make(map[string]map[string]*config.Config) 1367 // for mesh routes, build one VS per namespace+host 1368 meshRoutes := make(map[string]map[string]*config.Config) 1369 for _, obj := range kr.HTTPRoute { 1370 buildHTTPVirtualServices(ctx, obj, gatewayRoutes, meshRoutes) 1371 } 1372 } 1373 } 1374 1375 func TestExtractGatewayServices(t *testing.T) { 1376 tests := []struct { 1377 name string 1378 r GatewayResources 1379 kgw *k8s.GatewaySpec 1380 obj config.Config 1381 gatewayServices []string 1382 err *ConfigError 1383 }{ 1384 { 1385 name: "managed gateway", 1386 r: GatewayResources{Domain: "cluster.local"}, 1387 kgw: &k8s.GatewaySpec{ 1388 GatewayClassName: "istio", 1389 }, 1390 obj: config.Config{ 1391 Meta: config.Meta{ 1392 Name: "foo", 1393 Namespace: "default", 1394 }, 1395 }, 1396 gatewayServices: []string{"foo-istio.default.svc.cluster.local"}, 1397 }, 1398 { 1399 name: "managed gateway with name overridden", 1400 r: GatewayResources{Domain: "cluster.local"}, 1401 kgw: &k8s.GatewaySpec{ 1402 GatewayClassName: "istio", 1403 }, 1404 obj: config.Config{ 1405 Meta: config.Meta{ 1406 Name: "foo", 1407 Namespace: "default", 1408 Annotations: map[string]string{ 1409 gatewayNameOverride: "bar", 1410 }, 1411 }, 1412 }, 1413 gatewayServices: []string{"bar.default.svc.cluster.local"}, 1414 }, 1415 { 1416 name: "unmanaged gateway", 1417 r: GatewayResources{Domain: "domain"}, 1418 kgw: &k8s.GatewaySpec{ 1419 GatewayClassName: "istio", 1420 Addresses: []k8s.GatewayAddress{ 1421 { 1422 Value: "abc", 1423 }, 1424 { 1425 Type: func() *k8s.AddressType { 1426 t := k8s.HostnameAddressType 1427 return &t 1428 }(), 1429 Value: "example.com", 1430 }, 1431 { 1432 Type: func() *k8s.AddressType { 1433 t := k8s.IPAddressType 1434 return &t 1435 }(), 1436 Value: "1.2.3.4", 1437 }, 1438 }, 1439 }, 1440 obj: config.Config{ 1441 Meta: config.Meta{ 1442 Name: "foo", 1443 Namespace: "default", 1444 }, 1445 }, 1446 gatewayServices: []string{"abc.default.svc.domain", "example.com"}, 1447 err: &ConfigError{ 1448 Reason: InvalidAddress, 1449 Message: "only Hostname is supported, ignoring [1.2.3.4]", 1450 }, 1451 }, 1452 } 1453 for _, tt := range tests { 1454 t.Run(tt.name, func(t *testing.T) { 1455 gatewayServices, err := extractGatewayServices(tt.r, tt.kgw, tt.obj, classInfo{}) 1456 assert.Equal(t, gatewayServices, tt.gatewayServices) 1457 assert.Equal(t, err, tt.err) 1458 }) 1459 } 1460 }