github.com/cilium/cilium@v1.16.2/operator/pkg/model/translation/envoy_virtual_host_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 "regexp" 9 "sort" 10 "testing" 11 12 envoy_config_route_v3 "github.com/cilium/proxy/go/envoy/config/route/v3" 13 envoy_type_matcher_v3 "github.com/cilium/proxy/go/envoy/type/matcher/v3" 14 "github.com/stretchr/testify/assert" 15 "github.com/stretchr/testify/require" 16 17 "github.com/cilium/cilium/operator/pkg/model" 18 ) 19 20 func TestSortableRoute(t *testing.T) { 21 arr := SortableRoute{ 22 { 23 Name: "regex match short", 24 Match: &envoy_config_route_v3.RouteMatch{ 25 PathSpecifier: &envoy_config_route_v3.RouteMatch_SafeRegex{ 26 SafeRegex: &envoy_type_matcher_v3.RegexMatcher{ 27 Regex: "/.*", 28 }, 29 }, 30 }, 31 }, 32 { 33 Name: "regex match long", 34 Match: &envoy_config_route_v3.RouteMatch{ 35 PathSpecifier: &envoy_config_route_v3.RouteMatch_SafeRegex{ 36 SafeRegex: &envoy_type_matcher_v3.RegexMatcher{ 37 Regex: "/regex/.*/long", 38 }, 39 }, 40 }, 41 }, 42 { 43 Name: "regex match with one header", 44 Match: &envoy_config_route_v3.RouteMatch{ 45 PathSpecifier: &envoy_config_route_v3.RouteMatch_SafeRegex{ 46 SafeRegex: &envoy_type_matcher_v3.RegexMatcher{ 47 Regex: "/regex", 48 }, 49 }, 50 Headers: []*envoy_config_route_v3.HeaderMatcher{ 51 { 52 Name: "header1", 53 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 54 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 55 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 56 Exact: "value1", 57 }, 58 }, 59 }, 60 }, 61 }, 62 }, 63 }, 64 { 65 Name: "regex match with one header and one query", 66 Match: &envoy_config_route_v3.RouteMatch{ 67 PathSpecifier: &envoy_config_route_v3.RouteMatch_SafeRegex{ 68 SafeRegex: &envoy_type_matcher_v3.RegexMatcher{ 69 Regex: "/regex", 70 }, 71 }, 72 Headers: []*envoy_config_route_v3.HeaderMatcher{ 73 { 74 Name: "header1", 75 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 76 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 77 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 78 Exact: "value1", 79 }, 80 }, 81 }, 82 }, 83 }, 84 QueryParameters: []*envoy_config_route_v3.QueryParameterMatcher{ 85 { 86 Name: "query1", 87 QueryParameterMatchSpecifier: &envoy_config_route_v3.QueryParameterMatcher_PresentMatch{ 88 PresentMatch: true, 89 }, 90 }, 91 }, 92 }, 93 }, 94 { 95 Name: "regex match with two headers", 96 Match: &envoy_config_route_v3.RouteMatch{ 97 PathSpecifier: &envoy_config_route_v3.RouteMatch_SafeRegex{ 98 SafeRegex: &envoy_type_matcher_v3.RegexMatcher{ 99 Regex: "/regex", 100 }, 101 }, 102 Headers: []*envoy_config_route_v3.HeaderMatcher{ 103 { 104 Name: "header1", 105 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 106 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 107 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 108 Exact: "value1", 109 }, 110 }, 111 }, 112 }, 113 { 114 Name: "header2", 115 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 116 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 117 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 118 Exact: "value2", 119 }, 120 }, 121 }, 122 }, 123 }, 124 }, 125 }, 126 { 127 Name: "exact match short", 128 Match: &envoy_config_route_v3.RouteMatch{ 129 PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{ 130 Path: "/exact/match", 131 }, 132 }, 133 }, 134 { 135 Name: "exact match long", 136 Match: &envoy_config_route_v3.RouteMatch{ 137 PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{ 138 Path: "/exact/match/longest", 139 }, 140 }, 141 }, 142 { 143 Name: "exact match long with POST method", 144 Match: &envoy_config_route_v3.RouteMatch{ 145 PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{ 146 Path: "/exact/match/longest", 147 }, 148 Headers: []*envoy_config_route_v3.HeaderMatcher{ 149 { 150 Name: ":method", 151 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 152 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 153 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 154 Exact: "POST", 155 }, 156 }, 157 }, 158 }, 159 }, 160 }, 161 }, 162 { 163 Name: "exact match long with GET method", 164 Match: &envoy_config_route_v3.RouteMatch{ 165 PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{ 166 Path: "/exact/match/longest", 167 }, 168 Headers: []*envoy_config_route_v3.HeaderMatcher{ 169 { 170 Name: ":method", 171 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 172 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 173 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 174 Exact: "GET", 175 }, 176 }, 177 }, 178 }, 179 }, 180 }, 181 }, 182 { 183 Name: "exact match with one header", 184 Match: &envoy_config_route_v3.RouteMatch{ 185 PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{ 186 Path: "/exact/match/header", 187 }, 188 Headers: []*envoy_config_route_v3.HeaderMatcher{ 189 { 190 Name: "header1", 191 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 192 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 193 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 194 Exact: "value1", 195 }, 196 }, 197 }, 198 }, 199 }, 200 }, 201 }, 202 { 203 Name: "exact match with one header and one query", 204 Match: &envoy_config_route_v3.RouteMatch{ 205 PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{ 206 Path: "/exact/match/header", 207 }, 208 Headers: []*envoy_config_route_v3.HeaderMatcher{ 209 { 210 Name: "header1", 211 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 212 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 213 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 214 Exact: "value1", 215 }, 216 }, 217 }, 218 }, 219 }, 220 QueryParameters: []*envoy_config_route_v3.QueryParameterMatcher{ 221 { 222 Name: "query1", 223 QueryParameterMatchSpecifier: &envoy_config_route_v3.QueryParameterMatcher_PresentMatch{ 224 PresentMatch: true, 225 }, 226 }, 227 }, 228 }, 229 }, 230 { 231 Name: "exact match with two headers", 232 Match: &envoy_config_route_v3.RouteMatch{ 233 PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{ 234 Path: "/exact/match/header", 235 }, 236 Headers: []*envoy_config_route_v3.HeaderMatcher{ 237 { 238 Name: "header1", 239 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 240 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 241 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 242 Exact: "value1", 243 }, 244 }, 245 }, 246 }, 247 { 248 Name: "header2", 249 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 250 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 251 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 252 Exact: "value2", 253 }, 254 }, 255 }, 256 }, 257 }, 258 }, 259 }, 260 { 261 Name: "prefix match short", 262 Match: &envoy_config_route_v3.RouteMatch{ 263 PathSpecifier: &envoy_config_route_v3.RouteMatch_PathSeparatedPrefix{ 264 PathSeparatedPrefix: "/prefix/match", 265 }, 266 }, 267 }, 268 { 269 Name: "prefix match short with HEAD method", 270 Match: &envoy_config_route_v3.RouteMatch{ 271 PathSpecifier: &envoy_config_route_v3.RouteMatch_PathSeparatedPrefix{ 272 PathSeparatedPrefix: "/prefix/match", 273 }, 274 Headers: []*envoy_config_route_v3.HeaderMatcher{ 275 { 276 Name: ":method", 277 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 278 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 279 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 280 Exact: "HEAD", 281 }, 282 }, 283 }, 284 }, 285 }, 286 }, 287 }, 288 { 289 Name: "prefix match short with GET method", 290 Match: &envoy_config_route_v3.RouteMatch{ 291 PathSpecifier: &envoy_config_route_v3.RouteMatch_PathSeparatedPrefix{ 292 PathSeparatedPrefix: "/prefix/match", 293 }, 294 Headers: []*envoy_config_route_v3.HeaderMatcher{ 295 { 296 Name: ":method", 297 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 298 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 299 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 300 Exact: "GET", 301 }, 302 }, 303 }, 304 }, 305 }, 306 }, 307 }, 308 { 309 Name: "prefix match long", 310 Match: &envoy_config_route_v3.RouteMatch{ 311 PathSpecifier: &envoy_config_route_v3.RouteMatch_PathSeparatedPrefix{ 312 PathSeparatedPrefix: "/prefix/match/long", 313 }, 314 }, 315 }, 316 { 317 Name: "prefix match with one header", 318 Match: &envoy_config_route_v3.RouteMatch{ 319 PathSpecifier: &envoy_config_route_v3.RouteMatch_PathSeparatedPrefix{ 320 PathSeparatedPrefix: "/header", 321 }, 322 Headers: []*envoy_config_route_v3.HeaderMatcher{ 323 { 324 Name: "header1", 325 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 326 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 327 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 328 Exact: "value1", 329 }, 330 }, 331 }, 332 }, 333 }, 334 }, 335 }, 336 { 337 Name: "prefix match with one header and one query", 338 Match: &envoy_config_route_v3.RouteMatch{ 339 PathSpecifier: &envoy_config_route_v3.RouteMatch_PathSeparatedPrefix{ 340 PathSeparatedPrefix: "/header", 341 }, 342 Headers: []*envoy_config_route_v3.HeaderMatcher{ 343 { 344 Name: "header1", 345 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 346 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 347 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 348 Exact: "value1", 349 }, 350 }, 351 }, 352 }, 353 }, 354 QueryParameters: []*envoy_config_route_v3.QueryParameterMatcher{ 355 { 356 Name: "query1", 357 QueryParameterMatchSpecifier: &envoy_config_route_v3.QueryParameterMatcher_PresentMatch{ 358 PresentMatch: true, 359 }, 360 }, 361 }, 362 }, 363 }, 364 { 365 Name: "prefix match with two headers", 366 Match: &envoy_config_route_v3.RouteMatch{ 367 PathSpecifier: &envoy_config_route_v3.RouteMatch_PathSeparatedPrefix{ 368 PathSeparatedPrefix: "/header", 369 }, 370 Headers: []*envoy_config_route_v3.HeaderMatcher{ 371 { 372 Name: "header1", 373 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 374 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 375 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 376 Exact: "value1", 377 }, 378 }, 379 }, 380 }, 381 { 382 Name: "header2", 383 HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ 384 StringMatch: &envoy_type_matcher_v3.StringMatcher{ 385 MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ 386 Exact: "value2", 387 }, 388 }, 389 }, 390 }, 391 }, 392 }, 393 }, 394 } 395 396 // This assertion is to it easier to tell how 397 // the array is rearranged by the sorting. 398 // It also effectively ensures that buildNameSlice is 399 // working correctly. 400 namesBeforeSort := buildNameSlice(arr) 401 assert.Equal(t, []string{ 402 "regex match short", 403 "regex match long", 404 "regex match with one header", 405 "regex match with one header and one query", 406 "regex match with two headers", 407 "exact match short", 408 "exact match long", 409 "exact match long with POST method", 410 "exact match long with GET method", 411 "exact match with one header", 412 "exact match with one header and one query", 413 "exact match with two headers", 414 "prefix match short", 415 "prefix match short with HEAD method", 416 "prefix match short with GET method", 417 "prefix match long", 418 "prefix match with one header", 419 "prefix match with one header and one query", 420 "prefix match with two headers", 421 }, namesBeforeSort) 422 423 sort.Sort(arr) 424 425 namesAfterSort := buildNameSlice(arr) 426 assert.Equal(t, []string{ 427 "exact match long with GET method", 428 "exact match long with POST method", 429 "exact match long", 430 "exact match with two headers", 431 "exact match with one header and one query", 432 "exact match with one header", 433 "exact match short", 434 "regex match long", 435 "regex match with two headers", 436 "regex match with one header and one query", 437 "regex match with one header", 438 "regex match short", 439 "prefix match long", 440 "prefix match short with GET method", 441 "prefix match short with HEAD method", 442 "prefix match short", 443 "prefix match with two headers", 444 "prefix match with one header and one query", 445 "prefix match with one header", 446 }, namesAfterSort) 447 448 } 449 450 func buildNameSlice(arr []*envoy_config_route_v3.Route) []string { 451 452 var names []string 453 454 for _, entry := range arr { 455 names = append(names, entry.Name) 456 } 457 458 return names 459 } 460 461 func Test_hostRewriteMutation(t *testing.T) { 462 t.Run("no host rewrite", func(t *testing.T) { 463 route := &envoy_config_route_v3.Route_Route{ 464 Route: &envoy_config_route_v3.RouteAction{}, 465 } 466 res := hostRewriteMutation(nil)(route) 467 require.Equal(t, route, res) 468 }) 469 470 t.Run("with host rewrite", func(t *testing.T) { 471 route := &envoy_config_route_v3.Route_Route{ 472 Route: &envoy_config_route_v3.RouteAction{}, 473 } 474 rewrite := &model.HTTPURLRewriteFilter{ 475 HostName: model.AddressOf("example.com"), 476 } 477 478 res := hostRewriteMutation(rewrite)(route) 479 require.Equal(t, res.Route.HostRewriteSpecifier, &envoy_config_route_v3.RouteAction_HostRewriteLiteral{ 480 HostRewriteLiteral: "example.com", 481 }) 482 }) 483 } 484 485 func Test_pathPrefixMutation(t *testing.T) { 486 t.Run("no prefix rewrite", func(t *testing.T) { 487 route := &envoy_config_route_v3.Route_Route{ 488 Route: &envoy_config_route_v3.RouteAction{}, 489 } 490 res := pathPrefixMutation(nil, nil)(route) 491 require.Equal(t, route, res) 492 }) 493 494 t.Run("with prefix rewrite", func(t *testing.T) { 495 httpRoute := model.HTTPRoute{} 496 httpRoute.PathMatch.Prefix = "/strip-prefix" 497 route := &envoy_config_route_v3.Route_Route{ 498 Route: &envoy_config_route_v3.RouteAction{}, 499 } 500 rewrite := &model.HTTPURLRewriteFilter{ 501 Path: &model.StringMatch{ 502 Prefix: "/prefix", 503 }, 504 } 505 506 res := pathPrefixMutation(rewrite, &httpRoute)(route) 507 require.Equal(t, res.Route.PrefixRewrite, "/prefix") 508 }) 509 t.Run("with empty prefix rewrite", func(t *testing.T) { 510 httpRoute := model.HTTPRoute{} 511 httpRoute.PathMatch.Prefix = "/strip-prefix" 512 route := &envoy_config_route_v3.Route_Route{ 513 Route: &envoy_config_route_v3.RouteAction{}, 514 } 515 rewrite := &model.HTTPURLRewriteFilter{ 516 Path: &model.StringMatch{ 517 Prefix: "", 518 }, 519 } 520 521 res := pathPrefixMutation(rewrite, &httpRoute)(route) 522 require.EqualValues(t, &envoy_type_matcher_v3.RegexMatchAndSubstitute{ 523 Pattern: &envoy_type_matcher_v3.RegexMatcher{ 524 Regex: fmt.Sprintf(`^%s(/?)(.*)`, regexp.QuoteMeta(httpRoute.PathMatch.Prefix)), 525 }, 526 Substitution: `/\2`, 527 }, res.Route.RegexRewrite) 528 }) 529 t.Run("with slash prefix rewrite", func(t *testing.T) { 530 httpRoute := model.HTTPRoute{} 531 httpRoute.PathMatch.Prefix = "/strip-prefix" 532 route := &envoy_config_route_v3.Route_Route{ 533 Route: &envoy_config_route_v3.RouteAction{}, 534 } 535 rewrite := &model.HTTPURLRewriteFilter{ 536 Path: &model.StringMatch{ 537 Prefix: "/", 538 }, 539 } 540 541 res := pathPrefixMutation(rewrite, &httpRoute)(route) 542 require.EqualValues(t, &envoy_type_matcher_v3.RegexMatchAndSubstitute{ 543 Pattern: &envoy_type_matcher_v3.RegexMatcher{ 544 Regex: fmt.Sprintf(`^%s(/?)(.*)`, regexp.QuoteMeta(httpRoute.PathMatch.Prefix)), 545 }, 546 Substitution: `/\2`, 547 }, res.Route.RegexRewrite) 548 }) 549 } 550 551 func Test_requestMirrorMutation(t *testing.T) { 552 t.Run("no mirror", func(t *testing.T) { 553 route := &envoy_config_route_v3.Route_Route{ 554 Route: &envoy_config_route_v3.RouteAction{}, 555 } 556 res := requestMirrorMutation(nil)(route) 557 require.Equal(t, route, res) 558 }) 559 560 t.Run("with mirror", func(t *testing.T) { 561 route := &envoy_config_route_v3.Route_Route{ 562 Route: &envoy_config_route_v3.RouteAction{}, 563 } 564 mirror := []*model.HTTPRequestMirror{ 565 { 566 Backend: &model.Backend{ 567 Name: "dummy-service", 568 Namespace: "default", 569 Port: &model.BackendPort{ 570 Port: 8080, 571 Name: "http", 572 }, 573 }, 574 }, 575 { 576 Backend: &model.Backend{ 577 Name: "another-dummy-service", 578 Namespace: "default", 579 Port: &model.BackendPort{ 580 Port: 8080, 581 Name: "http", 582 }, 583 }, 584 }, 585 } 586 587 res := requestMirrorMutation(mirror)(route) 588 require.Len(t, res.Route.RequestMirrorPolicies, 2) 589 require.Equal(t, res.Route.RequestMirrorPolicies[0].Cluster, "default:dummy-service:8080") 590 require.Equal(t, res.Route.RequestMirrorPolicies[0].RuntimeFraction.DefaultValue.Numerator, uint32(100)) 591 require.Equal(t, res.Route.RequestMirrorPolicies[1].Cluster, "default:another-dummy-service:8080") 592 require.Equal(t, res.Route.RequestMirrorPolicies[1].RuntimeFraction.DefaultValue.Numerator, uint32(100)) 593 }) 594 }