github.com/cilium/cilium@v1.16.2/operator/pkg/model/translation/cec_translator_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package translation 5 6 import ( 7 "fmt" 8 "slices" 9 "testing" 10 11 envoy_config_cluster_v3 "github.com/cilium/proxy/go/envoy/config/cluster/v3" 12 envoy_config_core_v3 "github.com/cilium/proxy/go/envoy/config/core/v3" 13 envoy_config_listener "github.com/cilium/proxy/go/envoy/config/listener/v3" 14 envoy_config_route_v3 "github.com/cilium/proxy/go/envoy/config/route/v3" 15 envoy_http_connection_manager_v3 "github.com/cilium/proxy/go/envoy/extensions/filters/network/http_connection_manager/v3" 16 envoy_transport_sockets_tls_v3 "github.com/cilium/proxy/go/envoy/extensions/transport_sockets/tls/v3" 17 matcherv3 "github.com/cilium/proxy/go/envoy/type/matcher/v3" 18 "github.com/google/go-cmp/cmp" 19 "github.com/stretchr/testify/require" 20 "google.golang.org/protobuf/proto" 21 "google.golang.org/protobuf/testing/protocmp" 22 "google.golang.org/protobuf/types/known/durationpb" 23 24 "github.com/cilium/cilium/operator/pkg/model" 25 ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" 26 ) 27 28 func TestSharedIngressTranslator_getBackendServices(t *testing.T) { 29 type args struct { 30 m *model.Model 31 } 32 tests := []struct { 33 name string 34 args args 35 want []*ciliumv2.Service 36 }{ 37 { 38 name: "default backend listener", 39 args: args{ 40 m: defaultBackendModel, 41 }, 42 want: []*ciliumv2.Service{ 43 { 44 Name: "default-backend", 45 Namespace: "random-namespace", 46 Ports: []string{ 47 "8080", 48 }, 49 }, 50 }, 51 }, 52 { 53 name: "host rule listeners", 54 args: args{ 55 m: hostRulesModel, 56 }, 57 want: []*ciliumv2.Service{ 58 { 59 Name: "foo-bar-com", 60 Namespace: "random-namespace", 61 Ports: []string{ 62 "http", 63 }, 64 }, 65 { 66 Name: "wildcard-foo-com", 67 Namespace: "random-namespace", 68 Ports: []string{ 69 "8080", 70 }, 71 }, 72 }, 73 }, 74 { 75 name: "path rule listeners", 76 args: args{ 77 m: pathRulesModel, 78 }, 79 want: []*ciliumv2.Service{ 80 { 81 Name: "aaa-prefix", 82 Namespace: "random-namespace", 83 Ports: []string{ 84 "8080", 85 }, 86 }, 87 { 88 Name: "aaa-slash-bbb-prefix", 89 Namespace: "random-namespace", 90 Ports: []string{ 91 "8080", 92 }, 93 }, 94 { 95 Name: "aaa-slash-bbb-slash-prefix", 96 Namespace: "random-namespace", 97 Ports: []string{ 98 "8080", 99 }, 100 }, 101 { 102 Name: "foo-exact", 103 Namespace: "random-namespace", 104 Ports: []string{ 105 "8080", 106 }, 107 }, 108 { 109 Name: "foo-prefix", 110 Namespace: "random-namespace", 111 Ports: []string{ 112 "8080", 113 }, 114 }, 115 { 116 Name: "foo-slash-exact", 117 Namespace: "random-namespace", 118 Ports: []string{ 119 "8080", 120 }, 121 }, 122 }, 123 }, 124 { 125 name: "complex ingress", 126 args: args{ 127 m: complexIngressModel, 128 }, 129 want: []*ciliumv2.Service{ 130 { 131 Name: "another-dummy-backend", 132 Namespace: "dummy-namespace", 133 Ports: []string{"8081"}, 134 }, 135 { 136 Name: "default-backend", 137 Namespace: "dummy-namespace", 138 Ports: []string{"8080"}, 139 }, 140 { 141 Name: "dummy-backend", 142 Namespace: "dummy-namespace", 143 Ports: []string{"8080"}, 144 }, 145 }, 146 }, 147 } 148 for _, tt := range tests { 149 t.Run(tt.name, func(t *testing.T) { 150 i := &cecTranslator{} 151 res := i.getBackendServices(tt.args.m) 152 require.Equal(t, tt.want, res) 153 }) 154 } 155 } 156 157 func TestSharedIngressTranslator_getServices(t *testing.T) { 158 type fields struct { 159 name string 160 namespace string 161 } 162 tests := []struct { 163 name string 164 fields fields 165 model *model.Model 166 want []*ciliumv2.ServiceListener 167 }{ 168 { 169 name: "default case", 170 fields: fields{ 171 name: "cilium-ingress", 172 namespace: "kube-system", 173 }, 174 model: &model.Model{ 175 HTTP: []model.HTTPListener{ 176 { 177 Port: 80, 178 }, 179 { 180 Port: 443, 181 }, 182 }, 183 }, 184 want: []*ciliumv2.ServiceListener{ 185 { 186 Name: "cilium-ingress", 187 Namespace: "kube-system", 188 Ports: []uint16{ 189 80, 190 443, 191 }, 192 }, 193 }, 194 }, 195 { 196 name: "only http port", 197 fields: fields{ 198 name: "someotherservice", 199 namespace: "default", 200 }, 201 model: &model.Model{ 202 HTTP: []model.HTTPListener{ 203 { 204 Port: 80, 205 }, 206 }, 207 }, 208 want: []*ciliumv2.ServiceListener{ 209 { 210 Name: "someotherservice", 211 Namespace: "default", 212 Ports: []uint16{ 213 80, 214 }, 215 }, 216 }, 217 }, 218 { 219 name: "cleartext HTTP and TLS passthrough", 220 fields: fields{ 221 name: "cilium-ingress", 222 namespace: "kube-system", 223 }, 224 model: &model.Model{ 225 HTTP: []model.HTTPListener{ 226 { 227 Port: 80, 228 }, 229 }, 230 TLSPassthrough: []model.TLSPassthroughListener{ 231 { 232 Port: 443, 233 }, 234 }, 235 }, 236 want: []*ciliumv2.ServiceListener{ 237 { 238 Name: "cilium-ingress", 239 Namespace: "kube-system", 240 Ports: []uint16{ 241 80, 242 443, 243 }, 244 }, 245 }, 246 }, 247 } 248 249 for _, tt := range tests { 250 t.Run(tt.name, func(t *testing.T) { 251 i := &cecTranslator{} 252 got := i.getServicesWithPorts(tt.fields.namespace, tt.fields.name, tt.model) 253 require.Equal(t, tt.want, got) 254 }) 255 } 256 } 257 258 func TestSharedIngressTranslator_getListenerProxy(t *testing.T) { 259 i := &cecTranslator{ 260 secretsNamespace: "cilium-secrets", 261 useProxyProtocol: true, 262 } 263 res := i.getListener(&model.Model{ 264 HTTP: []model.HTTPListener{ 265 { 266 TLS: []model.TLSSecret{ 267 { 268 Name: "dummy-secret", 269 Namespace: "dummy-namespace", 270 }, 271 }, 272 }, 273 }, 274 }) 275 require.Len(t, res, 1) 276 listener := &envoy_config_listener.Listener{} 277 err := proto.Unmarshal(res[0].GetValue(), listener) 278 require.NoError(t, err) 279 280 listenerNames := []string{} 281 for _, l := range listener.ListenerFilters { 282 listenerNames = append(listenerNames, l.Name) 283 } 284 slices.Sort(listenerNames) 285 require.Equal(t, []string{proxyProtocolType, tlsInspectorType}, listenerNames) 286 } 287 288 func TestSharedIngressTranslator_getListener(t *testing.T) { 289 i := &cecTranslator{ 290 secretsNamespace: "cilium-secrets", 291 } 292 293 res := i.getListener(&model.Model{ 294 HTTP: []model.HTTPListener{ 295 { 296 TLS: []model.TLSSecret{ 297 { 298 Name: "dummy-secret", 299 Namespace: "dummy-namespace", 300 }, 301 }, 302 }, 303 }, 304 }) 305 require.Len(t, res, 1) 306 307 listener := &envoy_config_listener.Listener{} 308 err := proto.Unmarshal(res[0].GetValue(), listener) 309 require.NoError(t, err) 310 311 require.Len(t, listener.ListenerFilters, 1) 312 require.Len(t, listener.FilterChains, 2) 313 require.Len(t, listener.FilterChains[0].Filters, 1) 314 require.Len(t, listener.SocketOptions, 4) 315 require.IsType(t, &envoy_config_listener.Filter_TypedConfig{}, listener.FilterChains[0].Filters[0].ConfigType) 316 317 // check for connection manager 318 insecureConnectionManager := &envoy_http_connection_manager_v3.HttpConnectionManager{} 319 err = proto.Unmarshal(listener.FilterChains[0].Filters[0].ConfigType.(*envoy_config_listener.Filter_TypedConfig).TypedConfig.Value, insecureConnectionManager) 320 require.NoError(t, err) 321 322 require.Equal(t, "listener-insecure", insecureConnectionManager.StatPrefix) 323 require.Equal(t, "listener-insecure", insecureConnectionManager.GetRds().RouteConfigName) 324 325 secureConnectionManager := &envoy_http_connection_manager_v3.HttpConnectionManager{} 326 err = proto.Unmarshal(listener.FilterChains[1].Filters[0].ConfigType.(*envoy_config_listener.Filter_TypedConfig).TypedConfig.Value, secureConnectionManager) 327 require.NoError(t, err) 328 329 require.Equal(t, "listener-secure", secureConnectionManager.StatPrefix) 330 require.Equal(t, "listener-secure", secureConnectionManager.GetRds().RouteConfigName) 331 332 // check TLS configuration 333 require.Equal(t, "envoy.transport_sockets.tls", listener.FilterChains[1].TransportSocket.Name) 334 require.IsType(t, &envoy_config_core_v3.TransportSocket_TypedConfig{}, listener.FilterChains[1].TransportSocket.ConfigType) 335 336 downStreamTLS := &envoy_transport_sockets_tls_v3.DownstreamTlsContext{} 337 err = proto.Unmarshal(listener.FilterChains[1].TransportSocket.ConfigType.(*envoy_config_core_v3.TransportSocket_TypedConfig).TypedConfig.Value, downStreamTLS) 338 require.NoError(t, err) 339 340 require.Len(t, downStreamTLS.CommonTlsContext.TlsCertificateSdsSecretConfigs, 1) 341 require.Equal(t, downStreamTLS.CommonTlsContext.TlsCertificateSdsSecretConfigs[0].GetName(), "cilium-secrets/dummy-namespace-dummy-secret") 342 require.Nil(t, downStreamTLS.CommonTlsContext.TlsCertificateSdsSecretConfigs[0].GetSdsConfig()) 343 } 344 345 func TestSharedIngressTranslator_getClusters(t *testing.T) { 346 type args struct { 347 m *model.Model 348 } 349 tests := []struct { 350 name string 351 args args 352 expected []string 353 }{ 354 { 355 name: "default backend listener", 356 args: args{ 357 m: defaultBackendModel, 358 }, 359 expected: []string{ 360 "random-namespace:default-backend:8080", 361 }, 362 }, 363 { 364 name: "host rule listeners", 365 args: args{ 366 m: hostRulesModel, 367 }, 368 expected: []string{ 369 "random-namespace:foo-bar-com:http", 370 "random-namespace:wildcard-foo-com:8080", 371 }, 372 }, 373 { 374 name: "path rule listeners", 375 args: args{ 376 m: pathRulesModel, 377 }, 378 expected: []string{ 379 "random-namespace:aaa-prefix:8080", 380 "random-namespace:aaa-slash-bbb-prefix:8080", 381 "random-namespace:aaa-slash-bbb-slash-prefix:8080", 382 "random-namespace:foo-exact:8080", 383 "random-namespace:foo-prefix:8080", 384 "random-namespace:foo-slash-exact:8080", 385 }, 386 }, 387 { 388 name: "complex ingress", 389 args: args{ 390 m: complexIngressModel, 391 }, 392 expected: []string{ 393 "dummy-namespace:another-dummy-backend:8081", 394 "dummy-namespace:default-backend:8080", 395 "dummy-namespace:dummy-backend:8080", 396 }, 397 }, 398 } 399 400 for _, tt := range tests { 401 i := &cecTranslator{} 402 403 t.Run(tt.name, func(t *testing.T) { 404 res := i.getClusters(tt.args.m) 405 require.Len(t, res, len(tt.expected)) 406 407 for i := 0; i < len(tt.expected); i++ { 408 cluster := &envoy_config_cluster_v3.Cluster{} 409 err := proto.Unmarshal(res[i].GetValue(), cluster) 410 require.NoError(t, err) 411 412 require.Equal(t, tt.expected[i], cluster.Name) 413 require.Equal(t, &envoy_config_cluster_v3.Cluster_Type{Type: envoy_config_cluster_v3.Cluster_EDS}, cluster.ClusterDiscoveryType) 414 } 415 }) 416 } 417 } 418 419 func TestGetEnvoyHTTPRouteConfiguration_VirtualHostSorted(t *testing.T) { 420 defT := &cecTranslator{} 421 422 routes := []model.HTTPRoute{ 423 { 424 Backends: []model.Backend{ 425 { 426 Name: "default-backend", 427 Namespace: "random-namespace", 428 Port: &model.BackendPort{ 429 Port: 8080, 430 }, 431 }, 432 }, 433 }, 434 } 435 436 l1 := []model.HTTPListener{ 437 { 438 Port: 443, 439 Hostname: "foo.bar", 440 ForceHTTPtoHTTPSRedirect: true, 441 Routes: routes, 442 }, 443 { 444 Port: 443, 445 Hostname: "bar.foo", 446 Routes: routes, 447 }, 448 } 449 450 l2 := []model.HTTPListener{ 451 { 452 Port: 443, 453 Hostname: "bar.foo", 454 Routes: routes, 455 }, 456 { 457 Port: 443, 458 Hostname: "foo.bar", 459 ForceHTTPtoHTTPSRedirect: true, 460 Routes: routes, 461 }, 462 } 463 464 res1 := defT.getEnvoyHTTPRouteConfiguration(&model.Model{HTTP: l1}) 465 res2 := defT.getEnvoyHTTPRouteConfiguration(&model.Model{HTTP: l2}) 466 467 diffOutput := cmp.Diff(res1, res2, protocmp.Transform()) 468 if len(diffOutput) != 0 { 469 t.Errorf("CiliumEnvoyConfigs did not match:\n%s\n", diffOutput) 470 } 471 472 // assert.Equal(t, res1, res2) 473 } 474 475 func TestSharedIngressTranslator_getEnvoyHTTPRouteConfiguration(t *testing.T) { 476 type args struct { 477 m *model.Model 478 } 479 480 tests := []struct { 481 name string 482 args args 483 expectedRouteConfigs []*envoy_config_route_v3.RouteConfiguration 484 }{ 485 { 486 name: "default backend", 487 args: args{ 488 m: defaultBackendModel, 489 }, 490 expectedRouteConfigs: defaultBackendExpectedConfig, 491 }, 492 { 493 name: "host rule", 494 args: args{ 495 m: hostRulesModel, 496 }, 497 expectedRouteConfigs: hostRulesExpectedConfig, 498 }, 499 { 500 name: "host rule with enforceHTTPS", 501 args: args{ 502 m: hostRulesModelEnforcedHTTPS, 503 }, 504 expectedRouteConfigs: hostRulesExpectedConfigEnforceHTTPS, 505 }, 506 { 507 name: "path rules", 508 args: args{ 509 m: pathRulesModel, 510 }, 511 expectedRouteConfigs: pathRulesExpectedConfig, 512 }, 513 { 514 name: "complex ingress", 515 args: args{ 516 m: complexIngressModel, 517 }, 518 expectedRouteConfigs: complexIngressExpectedConfig, 519 }, 520 { 521 name: "complex ingress with enforceHTTPS", 522 args: args{ 523 m: complexIngressModelwithRedirects, 524 }, 525 expectedRouteConfigs: complexIngressExpectedConfigEnforceHTTPS, 526 }, 527 { 528 name: "multiple path types in one listener", 529 args: args{ 530 m: multiplePathTypesModel, 531 }, 532 expectedRouteConfigs: multiplePathTypesExpectedConfig, 533 }, 534 { 535 name: "routes with multiple hostnames in one listener", 536 args: args{ 537 m: multipleRouteHostnamesModel, 538 }, 539 expectedRouteConfigs: multipleRouteHostnamesExpectedConfig, 540 }, 541 } 542 543 defT := &cecTranslator{} 544 545 for _, tt := range tests { 546 t.Run(tt.name, func(t *testing.T) { 547 res := defT.getEnvoyHTTPRouteConfiguration(tt.args.m) 548 require.Len(t, res, len(tt.expectedRouteConfigs), "Number of Listeners did not match") 549 550 for i, rawRoute := range res { 551 ttListener := tt.expectedRouteConfigs[i] 552 listener := &envoy_config_route_v3.RouteConfiguration{} 553 err := proto.Unmarshal(rawRoute.Value, listener) 554 require.NoError(t, err) 555 556 for j, vhost := range listener.VirtualHosts { 557 if j >= len(ttListener.VirtualHosts) { 558 t.Errorf("More VirtualHosts in the actual than the expected for actual Listener name %s", listener.Name) 559 continue 560 } 561 562 ttVhost := ttListener.VirtualHosts[j] 563 564 if len(ttVhost.Routes) > len(vhost.Routes) { 565 diffOutput := cmp.Diff(ttVhost.Routes, vhost.Routes, protocmp.Transform()) 566 t.Errorf("More Routes in the actual than the expected for actual VirtualHost name %s in actual Listener %s\n%s\n", vhost.Name, listener.Name, diffOutput) 567 } 568 569 for k, route := range vhost.Routes { 570 if k >= len(ttVhost.Routes) { 571 continue 572 } 573 574 ttRoute := ttVhost.Routes[k] 575 576 diffOutput := cmp.Diff(ttRoute, route, protocmp.Transform()) 577 if len(diffOutput) != 0 { 578 t.Errorf("Routes did not match for Listener %s and VirtualHost %s, route number %d:\n%s\n", ttListener.Name, ttVhost.Name, k, diffOutput) 579 } 580 } 581 // If there were no errors at the Route level, check for errors at the VirtualHost level 582 if !t.Failed() { 583 diffOutput := cmp.Diff(ttVhost, vhost, protocmp.Transform()) 584 if len(diffOutput) != 0 { 585 t.Errorf("VirtualHosts did not match for Listener %s:\n %s\n", listener.Name, diffOutput) 586 } 587 } 588 } 589 // If there were no errors anywhere else, check for errors at the Listener level 590 if !t.Failed() { 591 diffOutput := cmp.Diff(ttListener, listener, protocmp.Transform()) 592 if len(diffOutput) != 0 { 593 t.Errorf("VirtualHosts did not match:\n %s\n", diffOutput) 594 } 595 } 596 597 } 598 }) 599 } 600 } 601 602 // The following helpers generate various types of path matches. 603 // Most notably, we treat a match for the path "/" differently to other matches, 604 // so it has its own helper. 605 606 func envoyRouteMatchExactPath(path string) *envoy_config_route_v3.RouteMatch { 607 return &envoy_config_route_v3.RouteMatch{ 608 PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{ 609 Path: path, 610 }, 611 } 612 } 613 614 func envoyRouteMatchImplementationSpecific(path string) *envoy_config_route_v3.RouteMatch { 615 return &envoy_config_route_v3.RouteMatch{ 616 PathSpecifier: &envoy_config_route_v3.RouteMatch_SafeRegex{ 617 SafeRegex: &matcherv3.RegexMatcher{ 618 Regex: path, 619 }, 620 }, 621 } 622 } 623 624 func envoyRouteMatchRootPath() *envoy_config_route_v3.RouteMatch { 625 return &envoy_config_route_v3.RouteMatch{ 626 PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{ 627 Prefix: "/", 628 }, 629 } 630 } 631 632 func envoyRouteMatchPrefixPath(path string) *envoy_config_route_v3.RouteMatch { 633 return &envoy_config_route_v3.RouteMatch{ 634 PathSpecifier: &envoy_config_route_v3.RouteMatch_PathSeparatedPrefix{ 635 PathSeparatedPrefix: path, 636 }, 637 } 638 } 639 640 func envoyRouteAction(namespace, backend, port string) *envoy_config_route_v3.Route_Route { 641 return &envoy_config_route_v3.Route_Route{ 642 Route: &envoy_config_route_v3.RouteAction{ 643 ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{ 644 Cluster: fmt.Sprintf("%s:%s:%s", namespace, backend, port), 645 }, 646 MaxStreamDuration: &envoy_config_route_v3.RouteAction_MaxStreamDuration{ 647 MaxStreamDuration: &durationpb.Duration{Seconds: 0}, 648 }, 649 }, 650 } 651 } 652 653 func envoyHTTPSRouteRedirect() *envoy_config_route_v3.Route_Redirect { 654 return &envoy_config_route_v3.Route_Redirect{ 655 Redirect: &envoy_config_route_v3.RedirectAction{ 656 SchemeRewriteSpecifier: &envoy_config_route_v3.RedirectAction_HttpsRedirect{ 657 HttpsRedirect: true, 658 }, 659 }, 660 } 661 } 662 663 func withAuthority(match *envoy_config_route_v3.RouteMatch, regex string) *envoy_config_route_v3.RouteMatch { 664 authorityHeader := &envoy_config_route_v3.HeaderMatcher{ 665 Name: ":authority", 666 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 667 StringMatch: &matcherv3.StringMatcher{ 668 MatchPattern: &matcherv3.StringMatcher_SafeRegex{ 669 SafeRegex: &matcherv3.RegexMatcher{ 670 Regex: regex, 671 }, 672 }, 673 }, 674 }, 675 } 676 677 match.Headers = append(match.Headers, authorityHeader) 678 679 return match 680 } 681 682 func domainsHelper(domain string) []string { 683 if domain == "*" { 684 return []string{domain} 685 } 686 687 return []string{domain, fmt.Sprintf("%s:*", domain)} 688 } 689 690 func TestSharedIngressTranslator_getResources(t *testing.T) { 691 type args struct { 692 m *model.Model 693 } 694 tests := []struct { 695 name string 696 args args 697 expected int 698 }{ 699 { 700 name: "default backend", 701 args: args{ 702 m: defaultBackendModel, 703 }, 704 expected: 3, 705 }, 706 { 707 name: "host rules", 708 args: args{ 709 m: hostRulesModel, 710 }, 711 expected: 5, 712 }, 713 { 714 name: "path rules", 715 args: args{ 716 m: pathRulesModel, 717 }, 718 expected: 8, 719 }, 720 { 721 name: "complex ingress", 722 args: args{ 723 m: complexIngressModel, 724 }, 725 expected: 6, 726 }, 727 } 728 for _, tt := range tests { 729 t.Run(tt.name, func(t *testing.T) { 730 i := &cecTranslator{} 731 got := i.getResources(tt.args.m) 732 require.Lenf(t, got, tt.expected, "expected %d resources, got %d", tt.expected, len(got)) 733 734 // Log for debugging purpose 735 for _, e := range got { 736 b, _ := e.MarshalJSON() 737 t.Logf("%s\n", b) 738 } 739 }) 740 } 741 }