github.com/cilium/cilium@v1.16.2/operator/pkg/model/translation/gateway-api/translator_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package gateway_api 5 6 import ( 7 "fmt" 8 "testing" 9 10 envoy_config_route_v3 "github.com/cilium/proxy/go/envoy/config/route/v3" 11 "github.com/google/go-cmp/cmp" 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 "google.golang.org/protobuf/testing/protocmp" 15 corev1 "k8s.io/api/core/v1" 16 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 "k8s.io/apimachinery/pkg/types" 18 gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1" 19 20 "github.com/cilium/cilium/operator/pkg/model" 21 "github.com/cilium/cilium/operator/pkg/model/translation" 22 ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" 23 slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1" 24 ) 25 26 func Test_translator_Translate(t *testing.T) { 27 type args struct { 28 m *model.Model 29 } 30 tests := []struct { 31 name string 32 args args 33 want *ciliumv2.CiliumEnvoyConfig 34 wantErr bool 35 }{ 36 { 37 name: "Basic HTTP Listener", 38 args: args{ 39 m: &model.Model{ 40 HTTP: basicHTTPListeners(80), 41 }, 42 }, 43 want: basicHTTPListenersCiliumEnvoyConfig, 44 }, 45 { 46 name: "Basic TLS SNI Listener", 47 args: args{ 48 m: &model.Model{ 49 TLSPassthrough: basicTLSListeners, 50 }, 51 }, 52 want: basicTLSListenersCiliumEnvoyConfig, 53 }, 54 { 55 name: "Conformance/HTTPRouteSimpleSameNamespace", 56 args: args{ 57 m: &model.Model{ 58 HTTP: simpleSameNamespaceHTTPListeners, 59 }, 60 }, 61 want: simpleSameNamespaceHTTPListenersCiliumEnvoyConfig, 62 }, 63 { 64 name: "Conformance/HTTPRouteBackendProtocolH2C", 65 args: args{ 66 m: &model.Model{ 67 HTTP: backendProtocolDisabledH2CHTTPListeners, 68 }, 69 }, 70 want: simpleSameNamespaceHTTPListenersCiliumEnvoyConfig, 71 }, 72 { 73 name: "Conformance/HTTPRouteCrossNamespace", 74 args: args{ 75 m: &model.Model{ 76 HTTP: crossNamespaceHTTPListeners, 77 }, 78 }, 79 want: crossNamespaceHTTPListenersCiliumEnvoyConfig, 80 }, 81 { 82 name: "Conformance/HTTPExactPathMatching", 83 args: args{ 84 m: &model.Model{ 85 HTTP: exactPathMatchingHTTPListeners, 86 }, 87 }, 88 want: exactPathMatchingHTTPListenersCiliumEnvoyConfig, 89 }, 90 { 91 name: "Conformance/HTTPRouteHeaderMatching", 92 args: args{ 93 m: &model.Model{ 94 HTTP: headerMatchingHTTPListeners, 95 }, 96 }, 97 want: headerMatchingHTTPCiliumEnvoyConfig, 98 }, 99 { 100 name: "Conformance/HTTPRouteHostnameIntersection", 101 args: args{ 102 m: &model.Model{ 103 HTTP: hostnameIntersectionHTTPListeners, 104 }, 105 }, 106 want: hostnameIntersectionHTTPListenersCiliumEnvoyConfig, 107 }, 108 { 109 name: "Conformance/HTTPRouteListenerHostnameMatching", 110 args: args{ 111 m: &model.Model{ 112 HTTP: listenerHostnameMatchingHTTPListeners, 113 }, 114 }, 115 want: listenerHostNameMatchingCiliumEnvoyConfig, 116 }, 117 { 118 name: "Conformance/HTTPRouteMatchingAcrossRoutes", 119 args: args{ 120 m: &model.Model{ 121 HTTP: matchingAcrossHTTPListeners, 122 }, 123 }, 124 want: matchingAcrossHTTPListenersCiliumEnvoyConfig, 125 }, 126 { 127 name: "Conformance/HTTPRouteMatching", 128 args: args{ 129 m: &model.Model{ 130 HTTP: matchingHTTPListeners, 131 }, 132 }, 133 want: matchingHTTPListenersCiliumEnvoyConfig, 134 }, 135 { 136 name: "Conformance/HTTPRouteMethodMatching", 137 args: args{ 138 m: &model.Model{ 139 HTTP: methodMatchingHTTPListeners, 140 }, 141 }, 142 want: methodMatchingHTTPListenersHTTPListenersCiliumEnvoyConfig, 143 }, 144 { 145 name: "Conformance/HTTPRouteQueryParamMatching", 146 args: args{ 147 m: &model.Model{ 148 HTTP: queryParamMatchingHTTPListeners, 149 }, 150 }, 151 want: queryParamMatchingHTTPListenersCiliumEnvoyConfig, 152 }, 153 { 154 name: "Conformance/HTTPRouteRequestHeaderModifier", 155 args: args{ 156 m: &model.Model{ 157 HTTP: requestHeaderModifierHTTPListeners, 158 }, 159 }, 160 want: requestHeaderModifierHTTPListenersCiliumEnvoyConfig, 161 }, 162 { 163 name: "Conformance/HTTPRouteBackendRefsRequestHeaderModifier", 164 args: args{ 165 m: &model.Model{ 166 HTTP: backendRefsRequestHeaderModifierHTTPListeners, 167 }, 168 }, 169 want: backendRefsRequestHeaderModifierHTTPListenersCiliumEnvoyConfig, 170 }, 171 { 172 name: "Conformance/HTTPRouteRequestRedirect", 173 args: args{ 174 m: &model.Model{ 175 HTTP: requestRedirectHTTPListeners, 176 }, 177 }, 178 want: requestRedirectHTTPListenersCiliumEnvoyConfig, 179 }, 180 { 181 name: "Conformance/HTTPRouteResponseHeaderModifier", 182 args: args{ 183 m: &model.Model{ 184 HTTP: responseHeaderModifierHTTPListeners, 185 }, 186 }, 187 want: responseHeaderModifierHTTPListenersCiliumEnvoyConfig, 188 }, 189 { 190 name: "Conformance/HTTPRouteBackendRefsResponseHeaderModifier", 191 args: args{ 192 m: &model.Model{ 193 HTTP: backendRefsResponseHeaderModifierHTTPListeners, 194 }, 195 }, 196 want: backendRefsResponseHeaderModifierHTTPListenersCiliumEnvoyConfig, 197 }, 198 { 199 name: "Conformance/HTTPRouteRewriteHost", 200 args: args{ 201 m: &model.Model{ 202 HTTP: rewriteHostHTTPListeners, 203 }, 204 }, 205 want: rewriteHostHTTPListenersCiliumEnvoyConfig, 206 }, 207 { 208 name: "Conformance/HTTPRouteRewritePath", 209 args: args{ 210 m: &model.Model{ 211 HTTP: rewritePathHTTPListeners, 212 }, 213 }, 214 want: rewritePathHTTPListenersCiliumEnvoyConfig, 215 }, 216 { 217 name: "Conformance/HTTPRouteRequestMirror", 218 args: args{ 219 m: &model.Model{ 220 HTTP: mirrorHTTPListeners, 221 }, 222 }, 223 want: mirrorHTTPListenersCiliumEnvoyConfig, 224 }, 225 { 226 name: "Conformance/HTTPRouteRequestRedirectWithMultiHTTPListeners", 227 args: args{ 228 m: &model.Model{ 229 HTTP: requestRedirectWithMultiHTTPListeners, 230 }, 231 }, 232 want: requestRedirectWithMultiHTTPListenersCiliumEnvoyConfig, 233 }, 234 } 235 for _, tt := range tests { 236 t.Run(tt.name, func(t *testing.T) { 237 trans := &gatewayAPITranslator{ 238 cecTranslator: translation.NewCECTranslator("cilium-secrets", false, false, true, 60, false, nil, false, false, 0), 239 } 240 cec, _, _, err := trans.Translate(tt.args.m) 241 require.Equal(t, tt.wantErr, err != nil, "Error mismatch") 242 require.Equal(t, tt.want, cec, "CiliumEnvoyConfig did not match") 243 }) 244 } 245 } 246 247 func Test_translator_TranslateResource(t *testing.T) { 248 type args struct { 249 m *model.Model 250 } 251 tests := []struct { 252 name string 253 args args 254 wantErr bool 255 validateFuncs []func(config *ciliumv2.CiliumEnvoyConfig) bool 256 }{ 257 { 258 name: "MultipleListenerGateway", 259 args: args{ 260 m: &model.Model{ 261 HTTP: multipleListenerGatewayListeners, 262 }, 263 }, 264 validateFuncs: []func(cec *ciliumv2.CiliumEnvoyConfig) bool{ 265 func(cec *ciliumv2.CiliumEnvoyConfig) bool { 266 resource := ciliumv2.XDSResource{ 267 Any: toAny(&envoy_config_route_v3.RouteConfiguration{ 268 Name: "listener-insecure", 269 VirtualHosts: []*envoy_config_route_v3.VirtualHost{ 270 { 271 Name: "example.com", 272 Domains: []string{ 273 "example.com", 274 "example.com:*", 275 }, 276 Routes: []*envoy_config_route_v3.Route{ 277 { 278 Match: &envoy_config_route_v3.RouteMatch{ 279 PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{ 280 Prefix: "/", 281 }, 282 }, 283 Action: &envoy_config_route_v3.Route_Redirect{ 284 Redirect: &envoy_config_route_v3.RedirectAction{ 285 SchemeRewriteSpecifier: &envoy_config_route_v3.RedirectAction_SchemeRedirect{ 286 SchemeRedirect: "https", 287 }, 288 PortRedirect: 443, 289 }, 290 }, 291 }, 292 }, 293 }, 294 }, 295 }), 296 } 297 298 expected, _ := resource.MarshalJSON() 299 got, _ := cec.Spec.Resources[1].MarshalJSON() 300 return assert.Equal(t, string(expected), string(got), "Route Configuration mismatch") 301 }, 302 func(cec *ciliumv2.CiliumEnvoyConfig) bool { 303 resource := ciliumv2.XDSResource{ 304 Any: toAny(&envoy_config_route_v3.RouteConfiguration{ 305 Name: "listener-secure", 306 VirtualHosts: []*envoy_config_route_v3.VirtualHost{ 307 { 308 Name: "example.com", 309 Domains: []string{ 310 "example.com", 311 "example.com:*", 312 }, 313 Routes: []*envoy_config_route_v3.Route{ 314 { 315 Match: &envoy_config_route_v3.RouteMatch{ 316 PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{ 317 Prefix: "/", 318 }, 319 }, 320 Action: toRouteAction("default", "my-service", "8080"), 321 }, 322 }, 323 }, 324 }, 325 }), 326 } 327 328 expected, _ := resource.MarshalJSON() 329 got, _ := cec.Spec.Resources[2].MarshalJSON() 330 return assert.Equal(t, string(expected), string(got), "Route Configuration mismatch") 331 }, 332 }, 333 }, 334 } 335 336 for _, tt := range tests { 337 t.Run(tt.name, func(t *testing.T) { 338 trans := &gatewayAPITranslator{ 339 cecTranslator: translation.NewCECTranslator("cilium-secrets", false, false, true, 60, false, nil, false, false, 0), 340 } 341 cec, _, _, err := trans.Translate(tt.args.m) 342 require.Equal(t, tt.wantErr, err != nil, "Error mismatch") 343 for _, fn := range tt.validateFuncs { 344 require.True(t, fn(cec), "Validation failed") 345 } 346 }) 347 } 348 } 349 350 func Test_translator_Translate_AppProtocol(t *testing.T) { 351 type args struct { 352 m *model.Model 353 } 354 tests := []struct { 355 name string 356 args args 357 want *ciliumv2.CiliumEnvoyConfig 358 wantErr bool 359 }{ 360 { 361 name: "Conformance/HTTPRouteBackendProtocolH2C", 362 args: args{ 363 m: &model.Model{ 364 HTTP: backendProtocolEnabledH2CHTTPListeners, 365 }, 366 }, 367 want: backendProtocolEnabledH2CHTTPListenersCiliumEnvoyConfig, 368 }, 369 } 370 for _, tt := range tests { 371 t.Run(tt.name, func(t *testing.T) { 372 trans := &gatewayAPITranslator{ 373 cecTranslator: translation.NewCECTranslator("cilium-secrets", false, true, true, 60, false, nil, false, false, 0), 374 } 375 cec, _, _, err := trans.Translate(tt.args.m) 376 require.Equal(t, tt.wantErr, err != nil, "Error mismatch") 377 require.Equal(t, tt.want, cec, "CiliumEnvoyConfig did not match") 378 }) 379 } 380 } 381 382 func Test_translator_Translate_HostNetwork(t *testing.T) { 383 type args struct { 384 m *model.Model 385 } 386 tests := []struct { 387 name string 388 args args 389 nodeLabelSelector *slim_metav1.LabelSelector 390 ipv4Enabled bool 391 ipv6Enabled bool 392 want *ciliumv2.CiliumEnvoyConfig 393 wantErr bool 394 }{ 395 { 396 name: "Basic HTTP Listener", 397 ipv4Enabled: true, 398 args: args{ 399 m: &model.Model{ 400 HTTP: basicHTTPListeners(80), 401 }, 402 }, 403 want: basicHostPortHTTPListenersCiliumEnvoyConfig("0.0.0.0", 80, nil), 404 }, 405 { 406 name: "Basic HTTP Listener with different port", 407 ipv4Enabled: true, 408 args: args{ 409 m: &model.Model{ 410 HTTP: basicHTTPListeners(55555), 411 }, 412 }, 413 want: basicHostPortHTTPListenersCiliumEnvoyConfig("0.0.0.0", 55555, nil), 414 }, 415 { 416 name: "Basic HTTP Listener with different port and IPv6", 417 ipv4Enabled: false, 418 ipv6Enabled: true, 419 args: args{ 420 m: &model.Model{ 421 HTTP: basicHTTPListeners(55555), 422 }, 423 }, 424 want: basicHostPortHTTPListenersCiliumEnvoyConfig("::", 55555, nil), 425 }, 426 { 427 name: "Basic HTTP Listener with LabelSelector", 428 ipv4Enabled: true, 429 nodeLabelSelector: &slim_metav1.LabelSelector{ 430 MatchLabels: map[string]slim_metav1.MatchLabelsValue{ 431 "a": "b", 432 }, 433 }, 434 args: args{ 435 m: &model.Model{ 436 HTTP: basicHTTPListeners(55555), 437 }, 438 }, 439 want: basicHostPortHTTPListenersCiliumEnvoyConfig("0.0.0.0", 55555, &slim_metav1.LabelSelector{MatchLabels: map[string]slim_metav1.MatchLabelsValue{"a": "b"}}), 440 }, 441 } 442 for _, tt := range tests { 443 translatorCases := []struct { 444 name string 445 gatewayAPITranslator *gatewayAPITranslator 446 }{ 447 { 448 name: "Without externalTrafficPolicy", 449 gatewayAPITranslator: &gatewayAPITranslator{ 450 cecTranslator: translation.NewCECTranslator("cilium-secrets", false, false, true, 60, true, tt.nodeLabelSelector, tt.ipv4Enabled, tt.ipv6Enabled, 0), 451 hostNetworkEnabled: true, 452 }, 453 }, 454 { 455 name: "With externalTrafficPolicy", 456 gatewayAPITranslator: &gatewayAPITranslator{ 457 cecTranslator: translation.NewCECTranslator("cilium-secrets", false, false, true, 60, true, tt.nodeLabelSelector, tt.ipv4Enabled, tt.ipv6Enabled, 0), 458 hostNetworkEnabled: true, 459 externalTrafficPolicy: "Cluster", 460 }, 461 }, 462 } 463 464 t.Run(tt.name, func(t *testing.T) { 465 for _, translatorCase := range translatorCases { 466 t.Run(translatorCase.name, func(t *testing.T) { 467 cec, svc, ep, err := translatorCase.gatewayAPITranslator.Translate(tt.args.m) 468 require.Equal(t, tt.wantErr, err != nil, "Error mismatch") 469 470 diffOutput := cmp.Diff(tt.want, cec, protocmp.Transform()) 471 if len(diffOutput) != 0 { 472 t.Errorf("CiliumEnvoyConfigs did not match:\n%s\n", diffOutput) 473 } 474 475 require.NotNil(t, svc) 476 assert.Equal(t, corev1.ServiceTypeClusterIP, svc.Spec.Type) 477 require.Emptyf(t, svc.Spec.ExternalTrafficPolicy, "ClusterIP Services must not have an ExternalTrafficPolicy") 478 479 require.NotNil(t, ep) 480 }) 481 } 482 }) 483 } 484 } 485 486 func Test_translator_Translate_WithXffNumTrustedHops(t *testing.T) { 487 type args struct { 488 m *model.Model 489 } 490 tests := []struct { 491 name string 492 args args 493 want *ciliumv2.CiliumEnvoyConfig 494 wantErr bool 495 }{ 496 { 497 name: "Basic HTTP Listener with XffNumTrustedHops", 498 args: args{ 499 m: &model.Model{ 500 HTTP: basicHTTPListeners(80), 501 }, 502 }, 503 want: basicHTTPListenersCiliumEnvoyConfigWithXff, 504 }, 505 } 506 for _, tt := range tests { 507 t.Run(tt.name, func(t *testing.T) { 508 trans := &gatewayAPITranslator{ 509 cecTranslator: translation.NewCECTranslator("cilium-secrets", false, false, true, 60, false, nil, false, false, 2), 510 hostNetworkEnabled: true, 511 } 512 cec, svc, ep, err := trans.Translate(tt.args.m) 513 require.Equal(t, tt.wantErr, err != nil, "Error mismatch") 514 515 diffOutput := cmp.Diff(tt.want, cec, protocmp.Transform()) 516 if len(diffOutput) != 0 { 517 t.Errorf("CiliumEnvoyConfigs did not match:\n%s\n", diffOutput) 518 } 519 520 require.NotNil(t, svc) 521 assert.Equal(t, corev1.ServiceTypeClusterIP, svc.Spec.Type) 522 523 require.NotNil(t, ep) 524 }) 525 } 526 } 527 528 func Test_getService(t *testing.T) { 529 type args struct { 530 resource *model.FullyQualifiedResource 531 allPorts []uint32 532 labels map[string]string 533 annotations map[string]string 534 externalTrafficPolicy string 535 } 536 tests := []struct { 537 name string 538 args args 539 want *corev1.Service 540 }{ 541 { 542 name: "long name - more than 64 characters", 543 args: args{ 544 resource: &model.FullyQualifiedResource{ 545 Name: "test-long-long-long-long-long-long-long-long-long-long-long-long-name", 546 Namespace: "default", 547 Version: "v1", 548 Kind: "Gateway", 549 UID: "57889650-380b-4c05-9a2e-3baee7fd5271", 550 }, 551 allPorts: []uint32{80}, 552 externalTrafficPolicy: "Cluster", 553 }, 554 want: &corev1.Service{ 555 ObjectMeta: metav1.ObjectMeta{ 556 Name: "cilium-gateway-test-long-long-long-long-long-long-lo-8tfth549c6", 557 Namespace: "default", 558 Labels: map[string]string{ 559 owningGatewayLabel: "test-long-long-long-long-long-long-long-long-long-lo-4bftbgh5ht", 560 "gateway.networking.k8s.io/gateway-name": "test-long-long-long-long-long-long-long-long-long-lo-4bftbgh5ht", 561 }, 562 OwnerReferences: []metav1.OwnerReference{ 563 { 564 APIVersion: gatewayv1beta1.GroupVersion.String(), 565 Kind: "Gateway", 566 Name: "test-long-long-long-long-long-long-long-long-long-long-long-long-name", 567 UID: types.UID("57889650-380b-4c05-9a2e-3baee7fd5271"), 568 Controller: model.AddressOf(true), 569 }, 570 }, 571 }, 572 Spec: corev1.ServiceSpec{ 573 Ports: []corev1.ServicePort{ 574 { 575 Name: fmt.Sprintf("port-%d", 80), 576 Port: 80, 577 Protocol: corev1.ProtocolTCP, 578 }, 579 }, 580 Type: corev1.ServiceTypeLoadBalancer, 581 ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyCluster, 582 }, 583 }, 584 }, 585 { 586 name: "externaltrafficpolicy set to local", 587 args: args{ 588 resource: &model.FullyQualifiedResource{ 589 Name: "test-externaltrafficpolicy-local", 590 Namespace: "default", 591 Version: "v1", 592 Kind: "Gateway", 593 UID: "41b82697-2d8d-4776-81b6-44d0bbac7faa", 594 }, 595 allPorts: []uint32{80}, 596 externalTrafficPolicy: "Local", 597 }, 598 want: &corev1.Service{ 599 ObjectMeta: metav1.ObjectMeta{ 600 Name: "cilium-gateway-test-externaltrafficpolicy-local", 601 Namespace: "default", 602 Labels: map[string]string{ 603 owningGatewayLabel: "test-externaltrafficpolicy-local", 604 "gateway.networking.k8s.io/gateway-name": "test-externaltrafficpolicy-local", 605 }, 606 OwnerReferences: []metav1.OwnerReference{ 607 { 608 APIVersion: gatewayv1beta1.GroupVersion.String(), 609 Kind: "Gateway", 610 Name: "test-externaltrafficpolicy-local", 611 UID: types.UID("41b82697-2d8d-4776-81b6-44d0bbac7faa"), 612 Controller: model.AddressOf(true), 613 }, 614 }, 615 }, 616 Spec: corev1.ServiceSpec{ 617 Ports: []corev1.ServicePort{ 618 { 619 Name: fmt.Sprintf("port-%d", 80), 620 Port: 80, 621 Protocol: corev1.ProtocolTCP, 622 }, 623 }, 624 Type: corev1.ServiceTypeLoadBalancer, 625 ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, 626 }, 627 }, 628 }, 629 } 630 for _, tt := range tests { 631 t.Run(tt.name, func(t *testing.T) { 632 got := getService(tt.args.resource, tt.args.allPorts, tt.args.labels, tt.args.annotations, tt.args.externalTrafficPolicy) 633 assert.Equalf(t, tt.want, got, "getService(%v, %v, %v, %v)", tt.args.resource, tt.args.allPorts, tt.args.labels, tt.args.annotations) 634 assert.Equal(t, true, len(got.Name) <= 63, "Service name is too long") 635 }) 636 } 637 }